RCNConfigExperiment.m 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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. @interface RCNConfigExperiment ()
  26. @property(nonatomic, strong)
  27. NSMutableArray<NSData *> *experimentPayloads; ///< Experiment payloads.
  28. @property(nonatomic, strong)
  29. NSMutableDictionary<NSString *, id> *experimentMetadata; ///< Experiment metadata
  30. @property(nonatomic, strong)
  31. NSMutableArray<NSData *> *activeExperimentPayloads; ///< Activated experiment payloads.
  32. @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager.
  33. @property(nonatomic, strong) FIRExperimentController *experimentController;
  34. @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter;
  35. @end
  36. @implementation RCNConfigExperiment
  37. /// Designated initializer
  38. - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager
  39. experimentController:(FIRExperimentController *)controller {
  40. self = [super init];
  41. if (self) {
  42. _experimentPayloads = [[NSMutableArray alloc] init];
  43. _experimentMetadata = [[NSMutableDictionary alloc] init];
  44. _activeExperimentPayloads = [[NSMutableArray alloc] init];
  45. _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init];
  46. [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
  47. [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
  48. // Locale needs to be hardcoded. See
  49. // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
  50. [_experimentStartTimeDateFormatter
  51. setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
  52. [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
  53. _DBManager = DBManager;
  54. _experimentController = controller;
  55. [self loadExperimentFromTable];
  56. }
  57. return self;
  58. }
  59. - (void)loadExperimentFromTable {
  60. if (!_DBManager) {
  61. return;
  62. }
  63. __weak RCNConfigExperiment *weakSelf = self;
  64. RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
  65. RCNConfigExperiment *strongSelf = weakSelf;
  66. if (strongSelf == nil) {
  67. return;
  68. }
  69. if (result[@RCNExperimentTableKeyPayload]) {
  70. [strongSelf->_experimentPayloads removeAllObjects];
  71. for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) {
  72. NSError *error;
  73. id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
  74. options:kNilOptions
  75. error:&error];
  76. if (!experimentPayloadJSON || error) {
  77. FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
  78. @"Experiment payload could not be parsed as JSON.");
  79. } else {
  80. [strongSelf->_experimentPayloads addObject:experiment];
  81. }
  82. }
  83. }
  84. if (result[@RCNExperimentTableKeyMetadata]) {
  85. strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy];
  86. }
  87. /// Load activated experiments payload and metadata.
  88. if (result[@RCNExperimentTableKeyActivePayload]) {
  89. [strongSelf->_activeExperimentPayloads removeAllObjects];
  90. for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) {
  91. NSError *error;
  92. id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
  93. options:kNilOptions
  94. error:&error];
  95. if (!experimentPayloadJSON || error) {
  96. FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
  97. @"Activated experiment payload could not be parsed as JSON.");
  98. } else {
  99. [strongSelf->_activeExperimentPayloads addObject:experiment];
  100. }
  101. }
  102. }
  103. };
  104. [_DBManager loadExperimentWithCompletionHandler:completionHandler];
  105. }
  106. - (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *)response {
  107. // cache fetched experiment payloads.
  108. [_experimentPayloads removeAllObjects];
  109. [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload];
  110. for (NSDictionary<NSString *, id> *experiment in response) {
  111. NSError *error;
  112. NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment
  113. options:kNilOptions
  114. error:&error];
  115. if (!JSONPayload || error) {
  116. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030",
  117. @"Invalid experiment payload to be serialized.");
  118. } else {
  119. [_experimentPayloads addObject:JSONPayload];
  120. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
  121. value:JSONPayload
  122. completionHandler:nil];
  123. }
  124. }
  125. }
  126. - (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler {
  127. FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init];
  128. // Get the last experiment start time prior to the latest payload.
  129. NSTimeInterval lastStartTime =
  130. [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
  131. // Update the last experiment start time with the latest payload.
  132. [self updateExperimentStartTime];
  133. [self.experimentController
  134. updateExperimentsWithServiceOrigin:kServiceOrigin
  135. events:lifecycleEvent
  136. policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest
  137. lastStartTime:lastStartTime
  138. payloads:_experimentPayloads
  139. completionHandler:handler];
  140. /// Update activated experiments payload and metadata in DB.
  141. [self updateActiveExperimentsInDB];
  142. }
  143. - (void)updateExperimentStartTime {
  144. NSTimeInterval existingLastStartTime =
  145. [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
  146. NSTimeInterval latestStartTime =
  147. [self latestStartTimeWithExistingLastStartTime:existingLastStartTime];
  148. _experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime);
  149. if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) {
  150. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
  151. @"Invalid fetched experiment metadata to be serialized.");
  152. return;
  153. }
  154. NSError *error;
  155. NSData *serializedExperimentMetadata =
  156. [NSJSONSerialization dataWithJSONObject:_experimentMetadata
  157. options:NSJSONWritingPrettyPrinted
  158. error:&error];
  159. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
  160. value:serializedExperimentMetadata
  161. completionHandler:nil];
  162. }
  163. - (void)updateActiveExperimentsInDB {
  164. /// Put current fetched experiment payloads into activated experiment DB.
  165. [_activeExperimentPayloads removeAllObjects];
  166. [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload];
  167. for (NSData *experiment in _experimentPayloads) {
  168. [_activeExperimentPayloads addObject:experiment];
  169. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
  170. value:experiment
  171. completionHandler:nil];
  172. }
  173. }
  174. - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime {
  175. return [self.experimentController
  176. latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime
  177. andPayloads:_experimentPayloads];
  178. }
  179. @end