ABTConditionalUserPropertyController.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // Copyright 2019 Google
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
  15. #import "FirebaseABTesting/Sources/ABTConstants.h"
  16. #import "FirebaseABTesting/Sources/Public/FIRLifecycleEvents.h"
  17. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  18. #import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
  19. @implementation ABTConditionalUserPropertyController {
  20. dispatch_queue_t _analyticOperationQueue;
  21. id<FIRAnalyticsInterop> _Nullable _analytics;
  22. }
  23. /// Returns the ABTConditionalUserPropertyController singleton.
  24. + (instancetype)sharedInstanceWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
  25. static ABTConditionalUserPropertyController *sharedInstance = nil;
  26. static dispatch_once_t onceToken = 0;
  27. dispatch_once(&onceToken, ^{
  28. sharedInstance = [[ABTConditionalUserPropertyController alloc] initWithAnalytics:analytics];
  29. });
  30. return sharedInstance;
  31. }
  32. - (instancetype)initWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
  33. self = [super init];
  34. if (self) {
  35. _analyticOperationQueue =
  36. dispatch_queue_create("com.google.FirebaseABTesting.analytics", DISPATCH_QUEUE_SERIAL);
  37. _analytics = analytics;
  38. }
  39. return self;
  40. }
  41. #pragma mark - experiments proxy methods on Firebase Analytics
  42. - (NSArray *)experimentsWithOrigin:(NSString *)origin {
  43. return [_analytics conditionalUserProperties:origin propertyNamePrefix:@""];
  44. }
  45. - (void)clearExperiment:(NSString *)experimentID
  46. variantID:(NSString *)variantID
  47. withOrigin:(NSString *)origin
  48. payload:(ABTExperimentPayload *)payload
  49. events:(FIRLifecycleEvents *)events {
  50. // Payload always overwrite event names.
  51. NSString *clearExperimentEventName = events.clearExperimentEventName;
  52. if (payload && payload.clearEventToLog && payload.clearEventToLog.length) {
  53. clearExperimentEventName = payload.clearEventToLog;
  54. }
  55. [_analytics clearConditionalUserProperty:experimentID
  56. forOrigin:origin
  57. clearEventName:clearExperimentEventName
  58. clearEventParameters:@{experimentID : variantID}];
  59. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000015", @"Clear Experiment ID %@, variant ID %@.",
  60. experimentID, variantID);
  61. }
  62. - (void)setExperimentWithOrigin:(NSString *)origin
  63. payload:(ABTExperimentPayload *)payload
  64. events:(FIRLifecycleEvents *)events
  65. policy:(ABTExperimentPayloadExperimentOverflowPolicy)policy {
  66. NSInteger maxNumOfExperiments = [self maxNumberOfExperimentsOfOrigin:origin];
  67. if (maxNumOfExperiments < 0) {
  68. return;
  69. }
  70. // Clear experiments if overflow
  71. NSArray *experiments = [self experimentsWithOrigin:origin];
  72. if (!experiments) {
  73. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000003",
  74. @"Failed to get conditional user properties from Firebase Analytics.");
  75. return;
  76. }
  77. if (maxNumOfExperiments <= experiments.count) {
  78. ABTExperimentPayloadExperimentOverflowPolicy overflowPolicy =
  79. [self overflowPolicyWithPayload:payload originalPolicy:policy];
  80. id experimentToClear = experiments.firstObject;
  81. if (overflowPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest &&
  82. experimentToClear) {
  83. NSString *expID = [self experimentIDOfExperiment:experimentToClear];
  84. NSString *varID = [self variantIDOfExperiment:experimentToClear];
  85. [self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
  86. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000016",
  87. @"Clear experiment ID %@ variant ID %@ due to "
  88. @"overflow policy.",
  89. expID, varID);
  90. } else {
  91. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000017",
  92. @"Experiment ID %@ variant ID %@ won't be set due to "
  93. @"overflow policy.",
  94. payload.experimentId, payload.variantId);
  95. return;
  96. }
  97. }
  98. // Clear experiment if other variant ID exists.
  99. NSString *experimentID = payload.experimentId;
  100. NSString *variantID = payload.variantId;
  101. for (id experiment in experiments) {
  102. NSString *expID = [self experimentIDOfExperiment:experiment];
  103. NSString *varID = [self variantIDOfExperiment:experiment];
  104. if ([expID isEqualToString:experimentID] && ![varID isEqualToString:variantID]) {
  105. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000018",
  106. @"Clear experiment ID %@ with variant ID %@ because "
  107. @"only one variant ID can be existed "
  108. @"at any time.",
  109. expID, varID);
  110. [self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
  111. }
  112. }
  113. // Set experiment
  114. NSDictionary<NSString *, id> *experiment = [self createExperimentFromOrigin:origin
  115. payload:payload
  116. events:events];
  117. [_analytics setConditionalUserProperty:experiment];
  118. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000019",
  119. @"Set conditional user property, experiment ID %@ with "
  120. @"variant ID %@ triggered event %@.",
  121. experimentID, variantID, payload.triggerEvent);
  122. // Log setEvent (experiment lifecycle event to be set when an experiment is set)
  123. [self logEventWithOrigin:origin payload:payload events:events];
  124. }
  125. - (NSMutableDictionary<NSString *, id> *)createExperimentFromOrigin:(NSString *)origin
  126. payload:(ABTExperimentPayload *)payload
  127. events:(FIRLifecycleEvents *)events {
  128. NSMutableDictionary<NSString *, id> *experiment = [[NSMutableDictionary alloc] init];
  129. NSString *experimentID = payload.experimentId;
  130. NSString *variantID = payload.variantId;
  131. NSDictionary *eventParams = @{experimentID : variantID};
  132. [experiment setValue:origin forKey:kABTExperimentDictionaryOriginKey];
  133. NSTimeInterval creationTimestamp = (double)(payload.experimentStartTimeMillis / ABT_MSEC_PER_SEC);
  134. [experiment setValue:@(creationTimestamp) forKey:kABTExperimentDictionaryCreationTimestampKey];
  135. [experiment setValue:experimentID forKey:kABTExperimentDictionaryExperimentIDKey];
  136. [experiment setValue:variantID forKey:kABTExperimentDictionaryVariantIDKey];
  137. // For the experiment to be immediately activated/triggered, its trigger event must be null.
  138. // Double check if payload's trigger event is empty string, it must be set to null to trigger.
  139. if (payload && payload.triggerEvent && payload.triggerEvent.length) {
  140. [experiment setValue:payload.triggerEvent forKey:kABTExperimentDictionaryTriggeredEventNameKey];
  141. } else {
  142. [experiment setValue:nil forKey:kABTExperimentDictionaryTriggeredEventNameKey];
  143. }
  144. // Set timeout event name and params.
  145. NSString *timeoutEventName = events.timeoutExperimentEventName;
  146. if (payload && payload.timeoutEventToLog && payload.timeoutEventToLog.length) {
  147. timeoutEventName = payload.timeoutEventToLog;
  148. }
  149. NSDictionary<NSString *, id> *timeoutEvent = [self eventDictionaryWithOrigin:origin
  150. eventName:timeoutEventName
  151. params:eventParams];
  152. [experiment setValue:timeoutEvent forKey:kABTExperimentDictionaryTimedOutEventKey];
  153. // Set trigger timeout information on how long to wait for trigger event.
  154. NSTimeInterval triggerTimeout = (double)(payload.triggerTimeoutMillis / ABT_MSEC_PER_SEC);
  155. [experiment setValue:@(triggerTimeout) forKey:kABTExperimentDictionaryTriggerTimeoutKey];
  156. // Set activate event name and params.
  157. NSString *activateEventName = events.activateExperimentEventName;
  158. if (payload && payload.activateEventToLog && payload.activateEventToLog.length) {
  159. activateEventName = payload.activateEventToLog;
  160. }
  161. NSDictionary<NSString *, id> *triggeredEvent = [self eventDictionaryWithOrigin:origin
  162. eventName:activateEventName
  163. params:eventParams];
  164. [experiment setValue:triggeredEvent forKey:kABTExperimentDictionaryTriggeredEventKey];
  165. // Set time to live information for how long the experiment lasts.
  166. NSTimeInterval timeToLive = (double)(payload.timeToLiveMillis / ABT_MSEC_PER_SEC);
  167. [experiment setValue:@(timeToLive) forKey:kABTExperimentDictionaryTimeToLiveKey];
  168. // Set expired event name and params.
  169. NSString *expiredEventName = events.expireExperimentEventName;
  170. if (payload && payload.ttlExpiryEventToLog && payload.ttlExpiryEventToLog.length) {
  171. expiredEventName = payload.ttlExpiryEventToLog;
  172. }
  173. NSDictionary<NSString *, id> *expiredEvent = [self eventDictionaryWithOrigin:origin
  174. eventName:expiredEventName
  175. params:eventParams];
  176. [experiment setValue:expiredEvent forKey:kABTExperimentDictionaryExpiredEventKey];
  177. return experiment;
  178. }
  179. - (NSDictionary<NSString *, id> *)
  180. eventDictionaryWithOrigin:(nonnull NSString *)origin
  181. eventName:(nonnull NSString *)eventName
  182. params:(nonnull NSDictionary<NSString *, NSString *> *)params {
  183. return @{
  184. kABTEventDictionaryOriginKey : origin,
  185. kABTEventDictionaryNameKey : eventName,
  186. kABTEventDictionaryTimestampKey : @([NSDate date].timeIntervalSince1970),
  187. kABTEventDictionaryParametersKey : params
  188. };
  189. }
  190. #pragma mark - experiment properties
  191. - (NSString *)experimentIDOfExperiment:(id)experiment {
  192. if (!experiment) {
  193. return @"";
  194. }
  195. return [experiment valueForKey:kABTExperimentDictionaryExperimentIDKey];
  196. }
  197. - (NSString *)variantIDOfExperiment:(id)experiment {
  198. if (!experiment) {
  199. return @"";
  200. }
  201. return [experiment valueForKey:kABTExperimentDictionaryVariantIDKey];
  202. }
  203. - (NSInteger)maxNumberOfExperimentsOfOrigin:(NSString *)origin {
  204. if (!_analytics) {
  205. return 0;
  206. }
  207. return [_analytics maxUserProperties:origin];
  208. }
  209. #pragma mark - analytics internal methods
  210. - (void)logEventWithOrigin:(NSString *)origin
  211. payload:(ABTExperimentPayload *)payload
  212. events:(FIRLifecycleEvents *)events {
  213. NSString *setExperimentEventName = events.setExperimentEventName;
  214. if (payload && payload.setEventToLog && payload.setEventToLog.length) {
  215. setExperimentEventName = payload.setEventToLog;
  216. }
  217. NSDictionary<NSString *, NSString *> *params;
  218. params = payload.experimentId ? @{payload.experimentId : payload.variantId} : @{};
  219. [_analytics logEventWithOrigin:origin name:setExperimentEventName parameters:params];
  220. }
  221. #pragma mark - helper
  222. - (BOOL)isExperiment:(id)experiment theSameAsPayload:(ABTExperimentPayload *)payload {
  223. NSString *experimentID = [self experimentIDOfExperiment:experiment];
  224. NSString *variantID = [self variantIDOfExperiment:experiment];
  225. return [experimentID isEqualToString:payload.experimentId] &&
  226. [variantID isEqualToString:payload.variantId];
  227. }
  228. - (ABTExperimentPayloadExperimentOverflowPolicy)
  229. overflowPolicyWithPayload:(ABTExperimentPayload *)payload
  230. originalPolicy:(ABTExperimentPayloadExperimentOverflowPolicy)originalPolicy {
  231. if ([payload overflowPolicyIsValid]) {
  232. return payload.overflowPolicy;
  233. }
  234. if (originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest ||
  235. originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest) {
  236. return originalPolicy;
  237. }
  238. return ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
  239. }
  240. @end