FIRExperimentController.m 15 KB

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