FIRExperimentController.m 15 KB

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