| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- // 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/ABTConditionalUserPropertyController.h"
- #import "FirebaseABTesting/Sources/ABTConstants.h"
- #import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRLifecycleEvents.h"
- #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
- #import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
- @implementation ABTConditionalUserPropertyController {
- dispatch_queue_t _analyticOperationQueue;
- id<FIRAnalyticsInterop> _Nullable _analytics;
- }
- /// Returns the ABTConditionalUserPropertyController singleton.
- + (instancetype)sharedInstanceWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
- static ABTConditionalUserPropertyController *sharedInstance = nil;
- static dispatch_once_t onceToken = 0;
- dispatch_once(&onceToken, ^{
- sharedInstance = [[ABTConditionalUserPropertyController alloc] initWithAnalytics:analytics];
- });
- return sharedInstance;
- }
- - (instancetype)initWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
- self = [super init];
- if (self) {
- _analyticOperationQueue =
- dispatch_queue_create("com.google.FirebaseABTesting.analytics", DISPATCH_QUEUE_SERIAL);
- _analytics = analytics;
- }
- return self;
- }
- #pragma mark - experiments proxy methods on Firebase Analytics
- - (NSArray *)experimentsWithOrigin:(NSString *)origin {
- return [_analytics conditionalUserProperties:origin propertyNamePrefix:@""];
- }
- - (void)clearExperiment:(NSString *)experimentID
- variantID:(NSString *)variantID
- withOrigin:(NSString *)origin
- payload:(ABTExperimentPayload *)payload
- events:(FIRLifecycleEvents *)events {
- // Payload always overwrite event names.
- NSString *clearExperimentEventName = events.clearExperimentEventName;
- if (payload && payload.clearEventToLog && payload.clearEventToLog.length) {
- clearExperimentEventName = payload.clearEventToLog;
- }
- [_analytics clearConditionalUserProperty:experimentID
- forOrigin:origin
- clearEventName:clearExperimentEventName
- clearEventParameters:@{experimentID : variantID}];
- FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000015", @"Clear Experiment ID %@, variant ID %@.",
- experimentID, variantID);
- }
- - (void)setExperimentWithOrigin:(NSString *)origin
- payload:(ABTExperimentPayload *)payload
- events:(FIRLifecycleEvents *)events
- policy:(ABTExperimentPayloadExperimentOverflowPolicy)policy {
- NSInteger maxNumOfExperiments = [self maxNumberOfExperimentsOfOrigin:origin];
- if (maxNumOfExperiments < 0) {
- return;
- }
- // Clear experiments if overflow
- NSArray *experiments = [self experimentsWithOrigin:origin];
- if (!experiments) {
- FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000003",
- @"Failed to get conditional user properties from Firebase Analytics.");
- return;
- }
- if (maxNumOfExperiments <= experiments.count) {
- ABTExperimentPayloadExperimentOverflowPolicy overflowPolicy =
- [self overflowPolicyWithPayload:payload originalPolicy:policy];
- id experimentToClear = experiments.firstObject;
- if (overflowPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest &&
- experimentToClear) {
- NSString *expID = [self experimentIDOfExperiment:experimentToClear];
- NSString *varID = [self variantIDOfExperiment:experimentToClear];
- [self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
- FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000016",
- @"Clear experiment ID %@ variant ID %@ due to "
- @"overflow policy.",
- expID, varID);
- } else {
- FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000017",
- @"Experiment ID %@ variant ID %@ won't be set due to "
- @"overflow policy.",
- payload.experimentId, payload.variantId);
- return;
- }
- }
- // Clear experiment if other variant ID exists.
- NSString *experimentID = payload.experimentId;
- NSString *variantID = payload.variantId;
- for (id experiment in experiments) {
- NSString *expID = [self experimentIDOfExperiment:experiment];
- NSString *varID = [self variantIDOfExperiment:experiment];
- if ([expID isEqualToString:experimentID] && ![varID isEqualToString:variantID]) {
- FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000018",
- @"Clear experiment ID %@ with variant ID %@ because "
- @"only one variant ID can be existed "
- @"at any time.",
- expID, varID);
- [self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
- }
- }
- // Set experiment
- NSDictionary<NSString *, id> *experiment = [self createExperimentFromOrigin:origin
- payload:payload
- events:events];
- [_analytics setConditionalUserProperty:experiment];
- FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000019",
- @"Set conditional user property, experiment ID %@ with "
- @"variant ID %@ triggered event %@.",
- experimentID, variantID, payload.triggerEvent);
- // Log setEvent (experiment lifecycle event to be set when an experiment is set)
- [self logEventWithOrigin:origin payload:payload events:events];
- }
- - (NSMutableDictionary<NSString *, id> *)createExperimentFromOrigin:(NSString *)origin
- payload:(ABTExperimentPayload *)payload
- events:(FIRLifecycleEvents *)events {
- NSMutableDictionary<NSString *, id> *experiment = [[NSMutableDictionary alloc] init];
- NSString *experimentID = payload.experimentId;
- NSString *variantID = payload.variantId;
- NSDictionary *eventParams = @{experimentID : variantID};
- [experiment setValue:origin forKey:kABTExperimentDictionaryOriginKey];
- NSTimeInterval creationTimestamp = (double)(payload.experimentStartTimeMillis / ABT_MSEC_PER_SEC);
- [experiment setValue:@(creationTimestamp) forKey:kABTExperimentDictionaryCreationTimestampKey];
- [experiment setValue:experimentID forKey:kABTExperimentDictionaryExperimentIDKey];
- [experiment setValue:variantID forKey:kABTExperimentDictionaryVariantIDKey];
- // For the experiment to be immediately activated/triggered, its trigger event must be null.
- // Double check if payload's trigger event is empty string, it must be set to null to trigger.
- if (payload && payload.triggerEvent && payload.triggerEvent.length) {
- [experiment setValue:payload.triggerEvent forKey:kABTExperimentDictionaryTriggeredEventNameKey];
- } else {
- [experiment setValue:nil forKey:kABTExperimentDictionaryTriggeredEventNameKey];
- }
- // Set timeout event name and params.
- NSString *timeoutEventName = events.timeoutExperimentEventName;
- if (payload && payload.timeoutEventToLog && payload.timeoutEventToLog.length) {
- timeoutEventName = payload.timeoutEventToLog;
- }
- NSDictionary<NSString *, id> *timeoutEvent = [self eventDictionaryWithOrigin:origin
- eventName:timeoutEventName
- params:eventParams];
- [experiment setValue:timeoutEvent forKey:kABTExperimentDictionaryTimedOutEventKey];
- // Set trigger timeout information on how long to wait for trigger event.
- NSTimeInterval triggerTimeout = (double)(payload.triggerTimeoutMillis / ABT_MSEC_PER_SEC);
- [experiment setValue:@(triggerTimeout) forKey:kABTExperimentDictionaryTriggerTimeoutKey];
- // Set activate event name and params.
- NSString *activateEventName = events.activateExperimentEventName;
- if (payload && payload.activateEventToLog && payload.activateEventToLog.length) {
- activateEventName = payload.activateEventToLog;
- }
- NSDictionary<NSString *, id> *triggeredEvent = [self eventDictionaryWithOrigin:origin
- eventName:activateEventName
- params:eventParams];
- [experiment setValue:triggeredEvent forKey:kABTExperimentDictionaryTriggeredEventKey];
- // Set time to live information for how long the experiment lasts.
- NSTimeInterval timeToLive = (double)(payload.timeToLiveMillis / ABT_MSEC_PER_SEC);
- [experiment setValue:@(timeToLive) forKey:kABTExperimentDictionaryTimeToLiveKey];
- // Set expired event name and params.
- NSString *expiredEventName = events.expireExperimentEventName;
- if (payload && payload.ttlExpiryEventToLog && payload.ttlExpiryEventToLog.length) {
- expiredEventName = payload.ttlExpiryEventToLog;
- }
- NSDictionary<NSString *, id> *expiredEvent = [self eventDictionaryWithOrigin:origin
- eventName:expiredEventName
- params:eventParams];
- [experiment setValue:expiredEvent forKey:kABTExperimentDictionaryExpiredEventKey];
- return experiment;
- }
- - (NSDictionary<NSString *, id> *)
- eventDictionaryWithOrigin:(nonnull NSString *)origin
- eventName:(nonnull NSString *)eventName
- params:(nonnull NSDictionary<NSString *, NSString *> *)params {
- return @{
- kABTEventDictionaryOriginKey : origin,
- kABTEventDictionaryNameKey : eventName,
- kABTEventDictionaryTimestampKey : @([NSDate date].timeIntervalSince1970),
- kABTEventDictionaryParametersKey : params
- };
- }
- #pragma mark - experiment properties
- - (NSString *)experimentIDOfExperiment:(id)experiment {
- if (!experiment) {
- return @"";
- }
- return [experiment valueForKey:kABTExperimentDictionaryExperimentIDKey];
- }
- - (NSString *)variantIDOfExperiment:(id)experiment {
- if (!experiment) {
- return @"";
- }
- return [experiment valueForKey:kABTExperimentDictionaryVariantIDKey];
- }
- - (NSInteger)maxNumberOfExperimentsOfOrigin:(NSString *)origin {
- if (!_analytics) {
- return 0;
- }
- return [_analytics maxUserProperties:origin];
- }
- #pragma mark - analytics internal methods
- - (void)logEventWithOrigin:(NSString *)origin
- payload:(ABTExperimentPayload *)payload
- events:(FIRLifecycleEvents *)events {
- NSString *setExperimentEventName = events.setExperimentEventName;
- if (payload && payload.setEventToLog && payload.setEventToLog.length) {
- setExperimentEventName = payload.setEventToLog;
- }
- NSDictionary<NSString *, NSString *> *params;
- params = payload.experimentId ? @{payload.experimentId : payload.variantId} : @{};
- [_analytics logEventWithOrigin:origin name:setExperimentEventName parameters:params];
- }
- #pragma mark - helper
- - (BOOL)isExperiment:(id)experiment theSameAsPayload:(ABTExperimentPayload *)payload {
- NSString *experimentID = [self experimentIDOfExperiment:experiment];
- NSString *variantID = [self variantIDOfExperiment:experiment];
- return [experimentID isEqualToString:payload.experimentId] &&
- [variantID isEqualToString:payload.variantId];
- }
- - (ABTExperimentPayloadExperimentOverflowPolicy)
- overflowPolicyWithPayload:(ABTExperimentPayload *)payload
- originalPolicy:(ABTExperimentPayloadExperimentOverflowPolicy)originalPolicy {
- if ([payload overflowPolicyIsValid]) {
- return payload.overflowPolicy;
- }
- if (originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest ||
- originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest) {
- return originalPolicy;
- }
- return ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
- }
- @end
|