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