RCNConfigExperiment.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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 "Protos/wireless/android/config/proto/Config.pbobjc.h"
  18. #import <FirebaseABTesting/ExperimentPayload.pbobjc.h>
  19. #import <FirebaseABTesting/FIRExperimentController.h>
  20. #import <FirebaseABTesting/FIRLifecycleEvents.h>
  21. #import <FirebaseCore/FIRLogger.h>
  22. #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
  23. #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
  24. static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time";
  25. /// Based on proto:
  26. /// http://google3/googlemac/iPhone/Firebase/ABTesting/Source/Protos/developers/mobile/abt/proto/ExperimentPayload.pbobjc.m
  27. static NSString *const kExperimentPayloadKeyExperimentID = @"experimentId";
  28. static NSString *const kExperimentPayloadKeyVariantID = @"variantId";
  29. static NSString *const kExperimentPayloadKeyExperimentStartTime = @"experimentStartTime";
  30. static NSString *const kExperimentPayloadKeyTriggerEvent = @"triggerEvent";
  31. static NSString *const kExperimentPayloadKeyTriggerTimeoutMillis = @"triggerTimeoutMillis";
  32. static NSString *const kExperimentPayloadKeyTimeToLiveMillis = @"timeToLiveMillis";
  33. static NSString *const kExperimentPayloadKeySetEventToLog = @"setEventToLog";
  34. static NSString *const kExperimentPayloadKeyActivateEventToLog = @"activateEventToLog";
  35. static NSString *const kExperimentPayloadKeyClearEventToLog = @"clearEventToLog";
  36. static NSString *const kExperimentPayloadKeyTimeoutEventToLog = @"timeoutEventToLog";
  37. static NSString *const kExperimentPayloadKeyTTLExpiryEventToLog = @"ttlExpiryEventToLog";
  38. static NSString *const kExperimentPayloadKeyOverflowPolicy = @"overflowPolicy";
  39. static NSString *const kServiceOrigin = @"frc";
  40. static NSString *const kMethodNameLatestStartTime =
  41. @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:";
  42. @interface RCNConfigExperiment ()
  43. @property(nonatomic, strong)
  44. NSMutableArray<NSData *> *experimentPayloads; ///< Experiment payloads.
  45. @property(nonatomic, strong)
  46. NSMutableDictionary<NSString *, id> *experimentMetadata; ///< Experiment metadata
  47. @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager.
  48. @property(nonatomic, strong) FIRExperimentController *experimentController;
  49. @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter;
  50. @end
  51. @implementation RCNConfigExperiment
  52. /// Designated initializer
  53. - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager
  54. experimentController:(FIRExperimentController *)controller {
  55. self = [super init];
  56. if (self) {
  57. _experimentPayloads = [[NSMutableArray alloc] init];
  58. _experimentMetadata = [[NSMutableDictionary alloc] init];
  59. _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init];
  60. [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
  61. [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
  62. // Locale needs to be hardcoded. See
  63. // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
  64. [_experimentStartTimeDateFormatter
  65. setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
  66. [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
  67. _DBManager = DBManager;
  68. _experimentController = controller;
  69. [self loadExperimentFromTable];
  70. }
  71. return self;
  72. }
  73. - (void)loadExperimentFromTable {
  74. if (!_DBManager) {
  75. return;
  76. }
  77. __weak RCNConfigExperiment *weakSelf = self;
  78. RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
  79. RCNConfigExperiment *strongSelf = weakSelf;
  80. if (strongSelf == nil) {
  81. return;
  82. }
  83. if (result[@RCNExperimentTableKeyPayload]) {
  84. [strongSelf->_experimentPayloads removeAllObjects];
  85. for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) {
  86. // Try to parse the experimentpayload as JSON.
  87. NSError *error;
  88. id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
  89. options:kNilOptions
  90. error:&error];
  91. if (!experimentPayloadJSON || error) {
  92. FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
  93. @"Experiment payload could not be parsed as JSON.");
  94. // Add this as serialized proto.
  95. [strongSelf->_experimentPayloads addObject:experiment];
  96. } else {
  97. // Convert to protobuf.
  98. NSData *protoPayload = [self convertABTExperimentPayloadToProto:experimentPayloadJSON];
  99. [strongSelf->_experimentPayloads addObject:protoPayload];
  100. }
  101. }
  102. }
  103. if (result[@RCNExperimentTableKeyMetadata]) {
  104. strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy];
  105. }
  106. };
  107. [_DBManager loadExperimentWithCompletionHandler:completionHandler];
  108. }
  109. /// This method converts the ABT experiment payload to a serialized protobuf which is consumable by
  110. /// the ABT SDK.
  111. - (NSData *)convertABTExperimentPayloadToProto:(NSDictionary<NSString *, id> *)experimentPayload {
  112. ABTExperimentPayload *ABTExperiment = [[ABTExperimentPayload alloc] init];
  113. ABTExperiment.experimentId = experimentPayload[kExperimentPayloadKeyExperimentID];
  114. ABTExperiment.variantId = experimentPayload[kExperimentPayloadKeyVariantID];
  115. NSDate *experimentStartTime = [self.experimentStartTimeDateFormatter
  116. dateFromString:experimentPayload[kExperimentPayloadKeyExperimentStartTime]];
  117. ABTExperiment.experimentStartTimeMillis =
  118. [@([experimentStartTime timeIntervalSince1970] * 1000) longLongValue];
  119. ABTExperiment.triggerEvent = experimentPayload[kExperimentPayloadKeyTriggerEvent];
  120. ABTExperiment.triggerTimeoutMillis =
  121. experimentPayload[kExperimentPayloadKeyTriggerTimeoutMillis]
  122. ? atoll([experimentPayload[kExperimentPayloadKeyTriggerTimeoutMillis] UTF8String])
  123. : 0;
  124. ABTExperiment.timeToLiveMillis =
  125. experimentPayload[kExperimentPayloadKeyTimeToLiveMillis]
  126. ? atoll([experimentPayload[kExperimentPayloadKeyTimeToLiveMillis] UTF8String])
  127. : 0;
  128. ABTExperiment.setEventToLog = experimentPayload[kExperimentPayloadKeySetEventToLog];
  129. ABTExperiment.activateEventToLog = experimentPayload[kExperimentPayloadKeyActivateEventToLog];
  130. ABTExperiment.clearEventToLog = experimentPayload[kExperimentPayloadKeyClearEventToLog];
  131. ABTExperiment.timeoutEventToLog = experimentPayload[kExperimentPayloadKeyTimeoutEventToLog];
  132. ABTExperiment.ttlExpiryEventToLog = experimentPayload[kExperimentPayloadKeyTTLExpiryEventToLog];
  133. ABTExperiment.overflowPolicy = [experimentPayload[kExperimentPayloadKeyOverflowPolicy] intValue];
  134. // Serialize the experiment payload.
  135. NSData *serializedABTExperiment = ABTExperiment.data;
  136. return serializedABTExperiment;
  137. }
  138. - (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *)response {
  139. // cache fetched experiment payloads.
  140. [_experimentPayloads removeAllObjects];
  141. [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload];
  142. for (NSDictionary<NSString *, id> *experiment in response) {
  143. NSData *protoPayload = [self convertABTExperimentPayloadToProto:experiment];
  144. [_experimentPayloads addObject:protoPayload];
  145. // We will add the new serialized JSON data to the database.
  146. // TODO: (b/129272809). Eventually, RC and ABT need to be migrated to move off protos once
  147. // (most) customers have migrated to using the new SDK (and hence saving the new JSON based
  148. // payload in the database).
  149. NSError *error;
  150. NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment
  151. options:kNilOptions
  152. error:&error];
  153. if (!JSONPayload || error) {
  154. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030",
  155. @"Invalid experiment payload to be serialized.");
  156. }
  157. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
  158. value:JSONPayload
  159. completionHandler:nil];
  160. }
  161. }
  162. - (void)updateExperiments {
  163. FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init];
  164. // Get the last experiment start time prior to the latest payload.
  165. NSTimeInterval lastStartTime =
  166. [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
  167. // Update the last experiment start time with the latest payload.
  168. [self updateExperimentStartTime];
  169. #pragma clang diagnostic push
  170. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  171. [self.experimentController
  172. updateExperimentsWithServiceOrigin:kServiceOrigin
  173. events:lifecycleEvent
  174. policy:ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest
  175. lastStartTime:lastStartTime
  176. payloads:_experimentPayloads];
  177. #pragma clang diagnostic pop
  178. }
  179. - (void)updateExperimentStartTime {
  180. NSTimeInterval existingLastStartTime =
  181. [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
  182. NSTimeInterval latestStartTime =
  183. [self latestStartTimeWithExistingLastStartTime:existingLastStartTime];
  184. _experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime);
  185. if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) {
  186. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
  187. @"Invalid fetched experiment metadata to be serialized.");
  188. return;
  189. }
  190. NSError *error;
  191. NSData *serializedExperimentMetadata =
  192. [NSJSONSerialization dataWithJSONObject:_experimentMetadata
  193. options:NSJSONWritingPrettyPrinted
  194. error:&error];
  195. [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
  196. value:serializedExperimentMetadata
  197. completionHandler:nil];
  198. }
  199. - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime {
  200. return [self.experimentController
  201. latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime
  202. andPayloads:_experimentPayloads];
  203. }
  204. @end