ABTConditionalUserPropertyController.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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/FirebaseABTesting/FIRLifecycleEvents.h"
  17. #import "FirebaseCore/Extension/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 (payload.experimentId == nil) {
  78. // When doing experiment test on devices, the payload could be empty. Returning here to prevent
  79. // app crash.
  80. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000020", @"Experiment Id in payload is empty.");
  81. return;
  82. }
  83. if (maxNumOfExperiments <= experiments.count) {
  84. ABTExperimentPayloadExperimentOverflowPolicy overflowPolicy =
  85. [self overflowPolicyWithPayload:payload originalPolicy:policy];
  86. id experimentToClear = experiments.firstObject;
  87. if (overflowPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest &&
  88. experimentToClear) {
  89. NSString *expID = [self experimentIDOfExperiment:experimentToClear];
  90. NSString *varID = [self variantIDOfExperiment:experimentToClear];
  91. [self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
  92. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000016",
  93. @"Clear experiment ID %@ variant ID %@ due to "
  94. @"overflow policy.",
  95. expID, varID);
  96. } else {
  97. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000017",
  98. @"Experiment ID %@ variant ID %@ won't be set due to "
  99. @"overflow policy.",
  100. payload.experimentId, payload.variantId);
  101. return;
  102. }
  103. }
  104. // Clear experiment if other variant ID exists.
  105. NSString *experimentID = payload.experimentId;
  106. NSString *variantID = payload.variantId;
  107. for (id experiment in experiments) {
  108. NSString *expID = [self experimentIDOfExperiment:experiment];
  109. NSString *varID = [self variantIDOfExperiment:experiment];
  110. if ([expID isEqualToString:experimentID] && ![varID isEqualToString:variantID]) {
  111. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000018",
  112. @"Clear experiment ID %@ with variant ID %@ because "
  113. @"only one variant ID can be existed "
  114. @"at any time.",
  115. expID, varID);
  116. [self clearExperiment:expID variantID:varID withOrigin:origin payload:payload events:events];
  117. }
  118. }
  119. // Set experiment
  120. NSDictionary<NSString *, id> *experiment = [self createExperimentFromOrigin:origin
  121. payload:payload
  122. events:events];
  123. [_analytics setConditionalUserProperty:experiment];
  124. FIRLogDebug(kFIRLoggerABTesting, @"I-ABT000019",
  125. @"Set conditional user property, experiment ID %@ with "
  126. @"variant ID %@ triggered event %@.",
  127. experimentID, variantID, payload.triggerEvent);
  128. // Log setEvent (experiment lifecycle event to be set when an experiment is set)
  129. [self logEventWithOrigin:origin payload:payload events:events];
  130. }
  131. - (NSMutableDictionary<NSString *, id> *)createExperimentFromOrigin:(NSString *)origin
  132. payload:(ABTExperimentPayload *)payload
  133. events:(FIRLifecycleEvents *)events {
  134. NSMutableDictionary<NSString *, id> *experiment = [[NSMutableDictionary alloc] init];
  135. NSString *experimentID = payload.experimentId;
  136. NSString *variantID = payload.variantId;
  137. NSDictionary *eventParams = @{experimentID : variantID};
  138. [experiment setValue:origin forKey:kABTExperimentDictionaryOriginKey];
  139. NSTimeInterval creationTimestamp = (double)(payload.experimentStartTimeMillis / ABT_MSEC_PER_SEC);
  140. [experiment setValue:@(creationTimestamp) forKey:kABTExperimentDictionaryCreationTimestampKey];
  141. [experiment setValue:experimentID forKey:kABTExperimentDictionaryExperimentIDKey];
  142. [experiment setValue:variantID forKey:kABTExperimentDictionaryVariantIDKey];
  143. // For the experiment to be immediately activated/triggered, its trigger event must be null.
  144. // Double check if payload's trigger event is empty string, it must be set to null to trigger.
  145. if (payload && payload.triggerEvent && payload.triggerEvent.length) {
  146. [experiment setValue:payload.triggerEvent forKey:kABTExperimentDictionaryTriggeredEventNameKey];
  147. } else {
  148. [experiment setValue:nil forKey:kABTExperimentDictionaryTriggeredEventNameKey];
  149. }
  150. // Set timeout event name and params.
  151. NSString *timeoutEventName = events.timeoutExperimentEventName;
  152. if (payload && payload.timeoutEventToLog && payload.timeoutEventToLog.length) {
  153. timeoutEventName = payload.timeoutEventToLog;
  154. }
  155. NSDictionary<NSString *, id> *timeoutEvent = [self eventDictionaryWithOrigin:origin
  156. eventName:timeoutEventName
  157. params:eventParams];
  158. [experiment setValue:timeoutEvent forKey:kABTExperimentDictionaryTimedOutEventKey];
  159. // Set trigger timeout information on how long to wait for trigger event.
  160. NSTimeInterval triggerTimeout = (double)(payload.triggerTimeoutMillis / ABT_MSEC_PER_SEC);
  161. [experiment setValue:@(triggerTimeout) forKey:kABTExperimentDictionaryTriggerTimeoutKey];
  162. // Set activate event name and params.
  163. NSString *activateEventName = events.activateExperimentEventName;
  164. if (payload && payload.activateEventToLog && payload.activateEventToLog.length) {
  165. activateEventName = payload.activateEventToLog;
  166. }
  167. NSDictionary<NSString *, id> *triggeredEvent = [self eventDictionaryWithOrigin:origin
  168. eventName:activateEventName
  169. params:eventParams];
  170. [experiment setValue:triggeredEvent forKey:kABTExperimentDictionaryTriggeredEventKey];
  171. // Set time to live information for how long the experiment lasts.
  172. NSTimeInterval timeToLive = (double)(payload.timeToLiveMillis / ABT_MSEC_PER_SEC);
  173. [experiment setValue:@(timeToLive) forKey:kABTExperimentDictionaryTimeToLiveKey];
  174. // Set expired event name and params.
  175. NSString *expiredEventName = events.expireExperimentEventName;
  176. if (payload && payload.ttlExpiryEventToLog && payload.ttlExpiryEventToLog.length) {
  177. expiredEventName = payload.ttlExpiryEventToLog;
  178. }
  179. NSDictionary<NSString *, id> *expiredEvent = [self eventDictionaryWithOrigin:origin
  180. eventName:expiredEventName
  181. params:eventParams];
  182. [experiment setValue:expiredEvent forKey:kABTExperimentDictionaryExpiredEventKey];
  183. return experiment;
  184. }
  185. - (NSDictionary<NSString *, id> *)
  186. eventDictionaryWithOrigin:(nonnull NSString *)origin
  187. eventName:(nonnull NSString *)eventName
  188. params:(nonnull NSDictionary<NSString *, NSString *> *)params {
  189. return @{
  190. kABTEventDictionaryOriginKey : origin,
  191. kABTEventDictionaryNameKey : eventName,
  192. kABTEventDictionaryTimestampKey : @([NSDate date].timeIntervalSince1970),
  193. kABTEventDictionaryParametersKey : params
  194. };
  195. }
  196. #pragma mark - experiment properties
  197. - (NSString *)experimentIDOfExperiment:(id)experiment {
  198. if (!experiment) {
  199. return @"";
  200. }
  201. return [experiment valueForKey:kABTExperimentDictionaryExperimentIDKey];
  202. }
  203. - (NSString *)variantIDOfExperiment:(id)experiment {
  204. if (!experiment) {
  205. return @"";
  206. }
  207. return [experiment valueForKey:kABTExperimentDictionaryVariantIDKey];
  208. }
  209. - (NSInteger)maxNumberOfExperimentsOfOrigin:(NSString *)origin {
  210. if (!_analytics) {
  211. return 0;
  212. }
  213. return [_analytics maxUserProperties:origin];
  214. }
  215. #pragma mark - analytics internal methods
  216. - (void)logEventWithOrigin:(NSString *)origin
  217. payload:(ABTExperimentPayload *)payload
  218. events:(FIRLifecycleEvents *)events {
  219. NSString *setExperimentEventName = events.setExperimentEventName;
  220. if (payload && payload.setEventToLog && payload.setEventToLog.length) {
  221. setExperimentEventName = payload.setEventToLog;
  222. }
  223. NSDictionary<NSString *, NSString *> *params;
  224. params = payload.experimentId ? @{payload.experimentId : payload.variantId} : @{};
  225. [_analytics logEventWithOrigin:origin name:setExperimentEventName parameters:params];
  226. }
  227. #pragma mark - helper
  228. - (BOOL)isExperiment:(id)experiment theSameAsPayload:(ABTExperimentPayload *)payload {
  229. NSString *experimentID = [self experimentIDOfExperiment:experiment];
  230. NSString *variantID = [self variantIDOfExperiment:experiment];
  231. return [experimentID isEqualToString:payload.experimentId] &&
  232. [variantID isEqualToString:payload.variantId];
  233. }
  234. - (ABTExperimentPayloadExperimentOverflowPolicy)
  235. overflowPolicyWithPayload:(ABTExperimentPayload *)payload
  236. originalPolicy:(ABTExperimentPayloadExperimentOverflowPolicy)originalPolicy {
  237. if ([payload overflowPolicyIsValid]) {
  238. return payload.overflowPolicy;
  239. }
  240. if (originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest ||
  241. originalPolicy == ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest) {
  242. return originalPolicy;
  243. }
  244. return ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
  245. }
  246. @end