ABTConditionalUserPropertyController.m 13 KB

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