FIRExperimentController.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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/Public/FirebaseABTesting/FIRExperimentController.h"
  15. #import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
  16. #import "FirebaseABTesting/Sources/ABTConstants.h"
  17. #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
  18. #import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRLifecycleEvents.h"
  19. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  20. #import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
  21. /// Logger Service String.
  22. FIRLoggerService kFIRLoggerABTesting = @"[FirebaseABTesting]";
  23. /// Default experiment overflow policy.
  24. const ABTExperimentPayloadExperimentOverflowPolicy FIRDefaultExperimentOverflowPolicy =
  25. ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
  26. /// Deserialize the experiment payloads.
  27. ABTExperimentPayload *ABTDeserializeExperimentPayload(NSData *payload) {
  28. // Verify that we have a JSON object.
  29. NSError *error;
  30. id JSONObject = [NSJSONSerialization JSONObjectWithData:payload options:kNilOptions error:&error];
  31. if (JSONObject == nil) {
  32. FIRLogError(kFIRLoggerABTesting, @"I-ABT000001", @"Failed to parse experiment payload: %@",
  33. error.debugDescription);
  34. }
  35. return [ABTExperimentPayload parseFromData:payload];
  36. }
  37. /// Returns a list of experiments to be set given the payloads and current list of experiments from
  38. /// Firebase Analytics. If an experiment is in payloads but not in experiments, it should be set to
  39. /// Firebase Analytics.
  40. NSArray<ABTExperimentPayload *> *ABTExperimentsToSetFromPayloads(
  41. NSArray<NSData *> *payloads,
  42. NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
  43. id<FIRAnalyticsInterop> _Nullable analytics) {
  44. NSArray<NSData *> *payloadsCopy = [payloads copy];
  45. NSArray *experimentsCopy = [experiments copy];
  46. NSMutableArray *experimentsToSet = [[NSMutableArray alloc] init];
  47. ABTConditionalUserPropertyController *controller =
  48. [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:analytics];
  49. // Check if the experiment is in payloads but not in experiments.
  50. for (NSData *payload in payloadsCopy) {
  51. ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
  52. if (!experimentPayload) {
  53. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
  54. @"Either payload is not set or it cannot be deserialized.");
  55. continue;
  56. }
  57. BOOL isExperimentSet = NO;
  58. for (id experiment in experimentsCopy) {
  59. if ([controller isExperiment:experiment theSameAsPayload:experimentPayload]) {
  60. isExperimentSet = YES;
  61. break;
  62. }
  63. }
  64. if (!isExperimentSet) {
  65. [experimentsToSet addObject:experimentPayload];
  66. }
  67. }
  68. return [experimentsToSet copy];
  69. }
  70. /// Returns a list of experiments to be cleared given the payloads and current list of
  71. /// experiments from Firebase Analytics. If an experiment is in experiments but not in payloads, it
  72. /// should be cleared in Firebase Analytics.
  73. NSArray *ABTExperimentsToClearFromPayloads(
  74. NSArray<NSData *> *payloads,
  75. NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
  76. id<FIRAnalyticsInterop> _Nullable analytics) {
  77. NSMutableArray *experimentsToClear = [[NSMutableArray alloc] init];
  78. ABTConditionalUserPropertyController *controller =
  79. [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:analytics];
  80. // Check if the experiment is in experiments but not payloads.
  81. for (id experiment in [experiments copy]) {
  82. BOOL doesExperimentNoLongerExist = YES;
  83. for (NSData *payload in [payloads copy]) {
  84. ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
  85. if (!experimentPayload) {
  86. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
  87. @"Either payload is not set or it cannot be deserialized.");
  88. continue;
  89. }
  90. if ([controller isExperiment:experiment theSameAsPayload:experimentPayload]) {
  91. doesExperimentNoLongerExist = NO;
  92. }
  93. }
  94. if (doesExperimentNoLongerExist) {
  95. [experimentsToClear addObject:experiment];
  96. }
  97. }
  98. return experimentsToClear;
  99. }
  100. // ABT doesn't provide any functionality to other components,
  101. // so it provides a private, empty protocol that it conforms to and use it for registration.
  102. @protocol FIRABTInstanceProvider
  103. @end
  104. @interface FIRExperimentController () <FIRABTInstanceProvider, FIRLibrary>
  105. @property(nonatomic, readwrite, strong) id<FIRAnalyticsInterop> _Nullable analytics;
  106. @end
  107. @implementation FIRExperimentController
  108. + (void)load {
  109. [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self withName:@"fire-abt"];
  110. }
  111. + (nonnull NSArray<FIRComponent *> *)componentsToRegister {
  112. FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop)
  113. isRequired:NO];
  114. FIRComponentCreationBlock creationBlock =
  115. ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
  116. // Ensure it's cached so it returns the same instance every time ABTesting is called.
  117. *isCacheable = YES;
  118. id<FIRAnalyticsInterop> analytics = FIR_COMPONENT(FIRAnalyticsInterop, container);
  119. return [[FIRExperimentController alloc] initWithAnalytics:analytics];
  120. };
  121. FIRComponent *abtProvider = [FIRComponent componentWithProtocol:@protocol(FIRABTInstanceProvider)
  122. instantiationTiming:FIRInstantiationTimingLazy
  123. dependencies:@[ analyticsDep ]
  124. creationBlock:creationBlock];
  125. return @[ abtProvider ];
  126. }
  127. - (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics {
  128. self = [super init];
  129. if (self != nil) {
  130. _analytics = analytics;
  131. }
  132. return self;
  133. }
  134. + (FIRExperimentController *)sharedInstance {
  135. FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here.
  136. id<FIRABTInstanceProvider> instance = FIR_COMPONENT(FIRABTInstanceProvider, defaultApp.container);
  137. // We know the instance coming from the container is a FIRExperimentController instance, cast it.
  138. return (FIRExperimentController *)instance;
  139. }
  140. - (void)updateExperimentsWithServiceOrigin:(NSString *)origin
  141. events:(FIRLifecycleEvents *)events
  142. policy:(ABTExperimentPayloadExperimentOverflowPolicy)policy
  143. lastStartTime:(NSTimeInterval)lastStartTime
  144. payloads:(NSArray<NSData *> *)payloads
  145. completionHandler:
  146. (nullable void (^)(NSError *_Nullable error))completionHandler {
  147. FIRExperimentController *__weak weakSelf = self;
  148. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
  149. FIRExperimentController *strongSelf = weakSelf;
  150. [strongSelf updateExperimentConditionalUserPropertiesWithServiceOrigin:origin
  151. events:events
  152. policy:policy
  153. lastStartTime:lastStartTime
  154. payloads:payloads
  155. completionHandler:completionHandler];
  156. });
  157. }
  158. - (void)
  159. updateExperimentConditionalUserPropertiesWithServiceOrigin:(NSString *)origin
  160. events:(FIRLifecycleEvents *)events
  161. policy:
  162. (ABTExperimentPayloadExperimentOverflowPolicy)
  163. policy
  164. lastStartTime:(NSTimeInterval)lastStartTime
  165. payloads:(NSArray<NSData *> *)payloads
  166. completionHandler:
  167. (nullable void (^)(NSError *_Nullable error))
  168. completionHandler {
  169. ABTConditionalUserPropertyController *controller =
  170. [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];
  171. // Get the list of expriments from Firebase Analytics.
  172. NSArray *experiments = [controller experimentsWithOrigin:origin];
  173. if (!experiments) {
  174. NSString *errorDescription =
  175. @"Failed to get conditional user properties from Firebase Analytics.";
  176. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000003", @"%@", errorDescription);
  177. if (completionHandler) {
  178. completionHandler([NSError
  179. errorWithDomain:kABTErrorDomain
  180. code:kABTInternalErrorFailedToFetchConditionalUserProperties
  181. userInfo:@{NSLocalizedDescriptionKey : errorDescription}]);
  182. }
  183. return;
  184. }
  185. NSArray<ABTExperimentPayload *> *experimentsToSet =
  186. ABTExperimentsToSetFromPayloads(payloads, experiments, _analytics);
  187. NSArray<NSDictionary<NSString *, NSString *> *> *experimentsToClear =
  188. ABTExperimentsToClearFromPayloads(payloads, experiments, _analytics);
  189. for (id experiment in experimentsToClear) {
  190. NSString *experimentID = [controller experimentIDOfExperiment:experiment];
  191. NSString *variantID = [controller variantIDOfExperiment:experiment];
  192. [controller clearExperiment:experimentID
  193. variantID:variantID
  194. withOrigin:origin
  195. payload:nil
  196. events:events];
  197. }
  198. for (ABTExperimentPayload *experimentPayload in experimentsToSet) {
  199. if (experimentPayload.experimentStartTimeMillis > lastStartTime * ABT_MSEC_PER_SEC) {
  200. [controller setExperimentWithOrigin:origin
  201. payload:experimentPayload
  202. events:events
  203. policy:policy];
  204. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000008",
  205. @"Set Experiment ID %@, variant ID %@ to Firebase Analytics.",
  206. experimentPayload.experimentId, experimentPayload.variantId);
  207. } else {
  208. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000009",
  209. @"Not setting experiment ID %@, variant ID %@ due to the last update time %lld.",
  210. experimentPayload.experimentId, experimentPayload.variantId,
  211. (long)lastStartTime * ABT_MSEC_PER_SEC);
  212. }
  213. }
  214. if (completionHandler) {
  215. completionHandler(nil);
  216. }
  217. }
  218. - (NSTimeInterval)latestExperimentStartTimestampBetweenTimestamp:(NSTimeInterval)timestamp
  219. andPayloads:(NSArray<NSData *> *)payloads {
  220. for (NSData *payload in [payloads copy]) {
  221. ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
  222. if (!experimentPayload) {
  223. FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
  224. @"Either payload is not set or it cannot be deserialized.");
  225. continue;
  226. }
  227. if (experimentPayload.experimentStartTimeMillis > timestamp * ABT_MSEC_PER_SEC) {
  228. timestamp = (double)(experimentPayload.experimentStartTimeMillis / ABT_MSEC_PER_SEC);
  229. }
  230. }
  231. return timestamp;
  232. }
  233. - (void)validateRunningExperimentsForServiceOrigin:(NSString *)origin
  234. runningExperimentPayloads:(NSArray<ABTExperimentPayload *> *)payloads {
  235. ABTConditionalUserPropertyController *controller =
  236. [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];
  237. FIRLifecycleEvents *lifecycleEvents = [[FIRLifecycleEvents alloc] init];
  238. // Get the list of experiments from Firebase Analytics.
  239. NSArray<NSDictionary<NSString *, NSString *> *> *activeExperiments =
  240. [controller experimentsWithOrigin:origin];
  241. NSMutableSet *runningExperimentIDs = [NSMutableSet setWithCapacity:payloads.count];
  242. for (ABTExperimentPayload *payload in payloads) {
  243. [runningExperimentIDs addObject:payload.experimentId];
  244. }
  245. for (NSDictionary<NSString *, NSString *> *activeExperimentDictionary in activeExperiments) {
  246. NSString *experimentID = activeExperimentDictionary[@"name"];
  247. if (![runningExperimentIDs containsObject:experimentID]) {
  248. NSString *variantID = activeExperimentDictionary[@"value"];
  249. [controller clearExperiment:experimentID
  250. variantID:variantID
  251. withOrigin:origin
  252. payload:nil
  253. events:lifecycleEvents];
  254. }
  255. }
  256. }
  257. - (void)activateExperiment:(ABTExperimentPayload *)experimentPayload
  258. forServiceOrigin:(NSString *)origin {
  259. ABTConditionalUserPropertyController *controller =
  260. [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];
  261. FIRLifecycleEvents *lifecycleEvents = [[FIRLifecycleEvents alloc] init];
  262. // Ensure that trigger event is nil, which will immediately set the experiment to active.
  263. [experimentPayload clearTriggerEvent];
  264. [controller setExperimentWithOrigin:origin
  265. payload:experimentPayload
  266. events:lifecycleEvents
  267. policy:experimentPayload.overflowPolicy];
  268. }
  269. @end