RCNConfigExperiment.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
  17. #import "FirebaseABTesting/Sources/Private/FirebaseABTestingInternal.h"
  18. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  19. #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
  20. #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
  21. static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time";
  22. static NSString *const kServiceOrigin = @"frc";
  23. static NSString *const kMethodNameLatestStartTime =
  24. @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:";
  25. static NSString *const kExperimentIdKey = @"experimentId";
  26. static NSString *const kAffectedParameterKeys = @"affectedParameterKeys";
  27. @interface RCNConfigExperiment ()
  28. @property(nonatomic, strong)
  29. NSMutableArray<NSData *> *experimentPayloads; ///< Experiment payloads.
  30. @property(nonatomic, strong)
  31. NSMutableDictionary<NSString *, id> *experimentMetadata; ///< Experiment metadata
  32. @property(nonatomic, strong)
  33. NSMutableArray<NSData *> *activeExperimentPayloads; ///< Activated experiment payloads.
  34. @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager.
  35. @property(nonatomic, strong) FIRExperimentController *experimentController;
  36. @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter;
  37. @end
  38. @implementation RCNConfigExperiment
  39. /// Designated initializer
  40. - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager
  41. experimentController:(FIRExperimentController *)controller {
  42. self = [super init];
  43. if (self) {
  44. _experimentPayloads = [[NSMutableArray alloc] init];
  45. _experimentMetadata = [[NSMutableDictionary alloc] init];
  46. _activeExperimentPayloads = [[NSMutableArray alloc] init];
  47. _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init];
  48. [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
  49. [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
  50. // Locale needs to be hardcoded. See
  51. // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
  52. [_experimentStartTimeDateFormatter
  53. setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
  54. [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
  55. _DBManager = DBManager;
  56. _experimentController = controller;
  57. [self loadExperimentFromTable];
  58. }
  59. return self;
  60. }
  61. - (void)loadExperimentFromTable {
  62. if (!_DBManager) {
  63. return;
  64. }
  65. __weak RCNConfigExperiment *weakSelf = self;
  66. RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
  67. RCNConfigExperiment *strongSelf = weakSelf;
  68. if (strongSelf == nil) {
  69. return;
  70. }
  71. if (result[@RCNExperimentTableKeyPayload]) {
  72. [strongSelf->_experimentPayloads removeAllObjects];
  73. for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) {
  74. NSError *error;
  75. id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
  76. options:kNilOptions
  77. error:&error];
  78. if (!experimentPayloadJSON || error) {
  79. FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
  80. @"Experiment payload could not be parsed as JSON.");
  81. } else {
  82. [strongSelf->_experimentPayloads addObject:experiment];
  83. }
  84. }
  85. }
  86. if (result[@RCNExperimentTableKeyMetadata]) {
  87. strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy];
  88. }
  89. /// Load activated experiments payload and metadata.
  90. if (result[@RCNExperimentTableKeyActivePayload]) {
  91. [strongSelf->_activeExperimentPayloads removeAllObjects];
  92. for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) {
  93. NSError *error;
  94. id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
  95. options:kNilOptions
  96. error:&error];
  97. if (!experimentPayloadJSON || error) {
  98. FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
  99. @"Activated experiment payload could not be parsed as JSON.");
  100. } else {
  101. [strongSelf->_activeExperimentPayloads addObject:experiment];
  102. }
  103. }
  104. }
  105. };
  106. [_DBManager loadExperimentWithCompletionHandler:completionHandler];
  107. }
  108. - (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *)response {
  109. // cache fetched experiment payloads.
  110. [_experimentPayloads removeAllObjects];
  111. [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload];
  112. for (NSDictionary<NSString *, id> *experiment in response) {
  113. NSError *error;
  114. NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment
  115. options:kNilOptions
  116. error:&error];
  117. if (!JSONPayload || error) {
  118. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030",
  119. @"Invalid experiment payload to be serialized.");
  120. } else {
  121. [_experimentPayloads addObject:JSONPayload];
  122. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
  123. value:JSONPayload
  124. completionHandler:nil];
  125. }
  126. }
  127. }
  128. - (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler {
  129. FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init];
  130. // Get the last experiment start time prior to the latest payload.
  131. NSTimeInterval lastStartTime =
  132. [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
  133. // Update the last experiment start time with the latest payload.
  134. [self updateExperimentStartTime];
  135. [self.experimentController
  136. updateExperimentsWithServiceOrigin:kServiceOrigin
  137. events:lifecycleEvent
  138. policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest
  139. lastStartTime:lastStartTime
  140. payloads:_experimentPayloads
  141. completionHandler:handler];
  142. /// Update activated experiments payload and metadata in DB.
  143. [self updateActiveExperimentsInDB];
  144. }
  145. - (void)updateExperimentStartTime {
  146. NSTimeInterval existingLastStartTime =
  147. [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
  148. NSTimeInterval latestStartTime =
  149. [self latestStartTimeWithExistingLastStartTime:existingLastStartTime];
  150. _experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime);
  151. if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) {
  152. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
  153. @"Invalid fetched experiment metadata to be serialized.");
  154. return;
  155. }
  156. NSError *error;
  157. NSData *serializedExperimentMetadata =
  158. [NSJSONSerialization dataWithJSONObject:_experimentMetadata
  159. options:NSJSONWritingPrettyPrinted
  160. error:&error];
  161. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
  162. value:serializedExperimentMetadata
  163. completionHandler:nil];
  164. }
  165. - (void)updateActiveExperimentsInDB {
  166. /// Put current fetched experiment payloads into activated experiment DB.
  167. [_activeExperimentPayloads removeAllObjects];
  168. [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload];
  169. for (NSData *experiment in _experimentPayloads) {
  170. [_activeExperimentPayloads addObject:experiment];
  171. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
  172. value:experiment
  173. completionHandler:nil];
  174. }
  175. }
  176. - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime {
  177. return [self.experimentController
  178. latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime
  179. andPayloads:_experimentPayloads];
  180. }
  181. - (NSMutableDictionary<NSString *, NSDictionary *> *)getExperimentsMap:
  182. (NSMutableArray<NSData *> *)experiments {
  183. NSMutableDictionary<NSString *, NSDictionary *> *experimentsMap =
  184. [[NSMutableDictionary alloc] init];
  185. for (NSData *experiment in experiments) {
  186. NSError *error;
  187. NSDictionary *experimentJSON =
  188. [NSJSONSerialization JSONObjectWithData:experiment
  189. options:NSJSONReadingMutableContainers
  190. error:&error];
  191. if (!error && experimentJSON) {
  192. /// Map experiments to experiment ID.
  193. [experimentsMap setObject:experimentJSON
  194. forKey:[experimentJSON valueForKey:kExperimentIdKey]];
  195. }
  196. }
  197. return experimentsMap;
  198. }
  199. - (NSMutableArray *)extractConfigKeysFromExperiment:(NSDictionary *)experiment {
  200. if (![experiment objectForKey:kAffectedParameterKeys]) {
  201. return [[NSMutableArray alloc] init];
  202. }
  203. return (NSMutableArray *)[experiment objectForKey:kAffectedParameterKeys];
  204. }
  205. - (bool)isExperimentMetadataUnchanged:(NSDictionary *)activeExperiment
  206. fetchedExperiment:(NSDictionary *)fetchedExperiment {
  207. /// Create copies of active and fetched experiments.
  208. NSMutableDictionary *activeExperimentCopy = [activeExperiment mutableCopy];
  209. NSMutableDictionary *fetchedExperimentCopy = [fetchedExperiment mutableCopy];
  210. /// Remove config parameter keys from object since they don't show up in consistent order.
  211. if ([activeExperimentCopy objectForKey:kAffectedParameterKeys]) {
  212. [activeExperimentCopy removeObjectForKey:kAffectedParameterKeys];
  213. }
  214. if ([fetchedExperimentCopy objectForKey:kAffectedParameterKeys]) {
  215. [fetchedExperimentCopy removeObjectForKey:kAffectedParameterKeys];
  216. }
  217. return [activeExperimentCopy isEqualToDictionary:fetchedExperimentCopy];
  218. }
  219. - (NSMutableSet<NSString *> *)getChangedExperimentConfigKeys:(NSMutableArray *)activeExperimentKeys
  220. fetchedExperimentKeys:
  221. (NSMutableArray *)fetchedExperimentKeys {
  222. NSMutableSet<NSString *> *allKeys = [[NSMutableSet alloc] init];
  223. NSMutableSet<NSString *> *activeKeys = [[NSMutableSet alloc] init];
  224. NSMutableSet<NSString *> *fetchedKeys = [[NSMutableSet alloc] init];
  225. /// Init keys set with experiment keys.
  226. [activeKeys addObjectsFromArray:activeExperimentKeys];
  227. [fetchedKeys addObjectsFromArray:fetchedExperimentKeys];
  228. /// Add all keys into a single set.
  229. allKeys = [[allKeys setByAddingObjectsFromSet:activeKeys] mutableCopy];
  230. allKeys = [[allKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy];
  231. NSMutableSet<NSString *> *changedKeys = [allKeys mutableCopy];
  232. /// Iterate through all possible keys.
  233. for (NSString *key in allKeys) {
  234. /// If keys are present in both active and fetched sets, remove from `changedKeys`.
  235. if ([activeKeys containsObject:key] && [fetchedKeys containsObject:key]) {
  236. [changedKeys removeObject:key];
  237. }
  238. }
  239. return changedKeys;
  240. }
  241. - (NSMutableSet<NSString *> *)getKeysAffectedByChangedExperiments {
  242. NSMutableSet<NSString *> *changedKeys = [[NSMutableSet alloc] init];
  243. NSMutableDictionary<NSString *, NSDictionary *> *activeExperiments =
  244. [self getExperimentsMap:_activeExperimentPayloads];
  245. NSMutableDictionary<NSString *, NSDictionary *> *fetchedExperiments =
  246. [self getExperimentsMap:_experimentPayloads];
  247. NSMutableSet<NSString *> *allExperimentIds = [[NSMutableSet alloc] init];
  248. [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]];
  249. [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]];
  250. /// Iterate through all possible experiment IDs.
  251. for (NSString *experimentId in allExperimentIds) {
  252. /// If an experiment ID doesn't exist one of the maps then an experiment must have been
  253. /// added/removed. Add it's keys into `changedKeys`.
  254. if (![activeExperiments objectForKey:experimentId] ||
  255. ![fetchedExperiments objectForKey:experimentId]) {
  256. /// Get the experiment that was altered.
  257. NSDictionary *experiment;
  258. if ([activeExperiments objectForKey:experimentId]) {
  259. experiment = [activeExperiments objectForKey:experimentId];
  260. } else {
  261. experiment = [fetchedExperiments objectForKey:experimentId];
  262. }
  263. /// Add all of it's keys into `changedKeys`.
  264. [changedKeys addObjectsFromArray:[self extractConfigKeysFromExperiment:experiment]];
  265. } else {
  266. /// Fetched and Active contain the experiment ID. The metadata needs to be compared to see if
  267. /// they're still the same.
  268. NSDictionary *activeExperiment = [activeExperiments objectForKey:experimentId];
  269. NSDictionary *fetchedExperiment = [fetchedExperiments objectForKey:experimentId];
  270. /// Extract keys from active and fetched experiments.
  271. NSMutableArray *activeExperimentKeys =
  272. [self extractConfigKeysFromExperiment:activeExperiment];
  273. NSMutableArray *fetchedExperimentKeys =
  274. [self extractConfigKeysFromExperiment:fetchedExperiment];
  275. if (![self isExperimentMetadataUnchanged:activeExperiment
  276. fetchedExperiment:fetchedExperiment]) {
  277. /// Add in all keys from both sides if the experiments metadata has changed.
  278. [changedKeys addObjectsFromArray:activeExperimentKeys];
  279. [changedKeys addObjectsFromArray:fetchedExperimentKeys];
  280. } else {
  281. /// Compare config keys from either experiment.
  282. changedKeys = [[changedKeys
  283. setByAddingObjectsFromSet:[self getChangedExperimentConfigKeys:activeExperimentKeys
  284. fetchedExperimentKeys:fetchedExperimentKeys]]
  285. mutableCopy];
  286. }
  287. }
  288. }
  289. return changedKeys;
  290. }
  291. @end