mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
225 lines
11 KiB
Objective-C
225 lines
11 KiB
Objective-C
/*
|
|
* Copyright 2019 Google
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
|
|
|
|
#import "Protos/wireless/android/config/proto/Config.pbobjc.h"
|
|
|
|
#import <FirebaseABTesting/FIRExperimentController.h>
|
|
#import <FirebaseABTesting/FIRLifecycleEvents.h>
|
|
#import <FirebaseCore/FIRLogger.h>
|
|
#import "FirebaseABTesting/Sources/Protos/developers/mobile/abt/proto/ExperimentPayload.pbobjc.h"
|
|
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
|
|
#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
|
|
|
|
static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time";
|
|
/// Based on proto:
|
|
/// http://google3/googlemac/iPhone/Firebase/ABTesting/Source/Protos/developers/mobile/abt/proto/ExperimentPayload.pbobjc.m
|
|
static NSString *const kExperimentPayloadKeyExperimentID = @"experimentId";
|
|
static NSString *const kExperimentPayloadKeyVariantID = @"variantId";
|
|
static NSString *const kExperimentPayloadKeyExperimentStartTime = @"experimentStartTime";
|
|
static NSString *const kExperimentPayloadKeyTriggerEvent = @"triggerEvent";
|
|
static NSString *const kExperimentPayloadKeyTriggerTimeoutMillis = @"triggerTimeoutMillis";
|
|
static NSString *const kExperimentPayloadKeyTimeToLiveMillis = @"timeToLiveMillis";
|
|
static NSString *const kExperimentPayloadKeySetEventToLog = @"setEventToLog";
|
|
static NSString *const kExperimentPayloadKeyActivateEventToLog = @"activateEventToLog";
|
|
static NSString *const kExperimentPayloadKeyClearEventToLog = @"clearEventToLog";
|
|
static NSString *const kExperimentPayloadKeyTimeoutEventToLog = @"timeoutEventToLog";
|
|
static NSString *const kExperimentPayloadKeyTTLExpiryEventToLog = @"ttlExpiryEventToLog";
|
|
static NSString *const kExperimentPayloadKeyOverflowPolicy = @"overflowPolicy";
|
|
|
|
static NSString *const kServiceOrigin = @"frc";
|
|
static NSString *const kMethodNameLatestStartTime =
|
|
@"latestExperimentStartTimestampBetweenTimestamp:andPayloads:";
|
|
|
|
@interface RCNConfigExperiment ()
|
|
@property(nonatomic, strong)
|
|
NSMutableArray<NSData *> *experimentPayloads; ///< Experiment payloads.
|
|
@property(nonatomic, strong)
|
|
NSMutableDictionary<NSString *, id> *experimentMetadata; ///< Experiment metadata
|
|
@property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager.
|
|
@property(nonatomic, strong) FIRExperimentController *experimentController;
|
|
@property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter;
|
|
@end
|
|
|
|
@implementation RCNConfigExperiment
|
|
/// Designated initializer
|
|
- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager
|
|
experimentController:(FIRExperimentController *)controller {
|
|
self = [super init];
|
|
if (self) {
|
|
_experimentPayloads = [[NSMutableArray alloc] init];
|
|
_experimentMetadata = [[NSMutableDictionary alloc] init];
|
|
_experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init];
|
|
[_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
|
|
[_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
|
// Locale needs to be hardcoded. See
|
|
// https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
|
|
[_experimentStartTimeDateFormatter
|
|
setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
|
|
[_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
|
|
|
|
_DBManager = DBManager;
|
|
_experimentController = controller;
|
|
[self loadExperimentFromTable];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)loadExperimentFromTable {
|
|
if (!_DBManager) {
|
|
return;
|
|
}
|
|
__weak RCNConfigExperiment *weakSelf = self;
|
|
RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
|
|
RCNConfigExperiment *strongSelf = weakSelf;
|
|
if (strongSelf == nil) {
|
|
return;
|
|
}
|
|
if (result[@RCNExperimentTableKeyPayload]) {
|
|
[strongSelf->_experimentPayloads removeAllObjects];
|
|
for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) {
|
|
// Try to parse the experimentpayload as JSON.
|
|
NSError *error;
|
|
id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
|
|
options:kNilOptions
|
|
error:&error];
|
|
if (!experimentPayloadJSON || error) {
|
|
FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
|
|
@"Experiment payload could not be parsed as JSON.");
|
|
// Add this as serialized proto.
|
|
[strongSelf->_experimentPayloads addObject:experiment];
|
|
} else {
|
|
// Convert to protobuf.
|
|
NSData *protoPayload = [self convertABTExperimentPayloadToProto:experimentPayloadJSON];
|
|
[strongSelf->_experimentPayloads addObject:protoPayload];
|
|
}
|
|
}
|
|
}
|
|
if (result[@RCNExperimentTableKeyMetadata]) {
|
|
strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy];
|
|
}
|
|
};
|
|
[_DBManager loadExperimentWithCompletionHandler:completionHandler];
|
|
}
|
|
|
|
/// This method converts the ABT experiment payload to a serialized protobuf which is consumable by
|
|
/// the ABT SDK.
|
|
- (NSData *)convertABTExperimentPayloadToProto:(NSDictionary<NSString *, id> *)experimentPayload {
|
|
ABTExperimentPayload *ABTExperiment = [[ABTExperimentPayload alloc] init];
|
|
ABTExperiment.experimentId = experimentPayload[kExperimentPayloadKeyExperimentID];
|
|
ABTExperiment.variantId = experimentPayload[kExperimentPayloadKeyVariantID];
|
|
NSDate *experimentStartTime = [self.experimentStartTimeDateFormatter
|
|
dateFromString:experimentPayload[kExperimentPayloadKeyExperimentStartTime]];
|
|
ABTExperiment.experimentStartTimeMillis =
|
|
[@([experimentStartTime timeIntervalSince1970] * 1000) longLongValue];
|
|
ABTExperiment.triggerEvent = experimentPayload[kExperimentPayloadKeyTriggerEvent];
|
|
ABTExperiment.triggerTimeoutMillis =
|
|
experimentPayload[kExperimentPayloadKeyTriggerTimeoutMillis]
|
|
? atoll([experimentPayload[kExperimentPayloadKeyTriggerTimeoutMillis] UTF8String])
|
|
: 0;
|
|
ABTExperiment.timeToLiveMillis =
|
|
experimentPayload[kExperimentPayloadKeyTimeToLiveMillis]
|
|
? atoll([experimentPayload[kExperimentPayloadKeyTimeToLiveMillis] UTF8String])
|
|
: 0;
|
|
ABTExperiment.setEventToLog = experimentPayload[kExperimentPayloadKeySetEventToLog];
|
|
ABTExperiment.activateEventToLog = experimentPayload[kExperimentPayloadKeyActivateEventToLog];
|
|
ABTExperiment.clearEventToLog = experimentPayload[kExperimentPayloadKeyClearEventToLog];
|
|
ABTExperiment.timeoutEventToLog = experimentPayload[kExperimentPayloadKeyTimeoutEventToLog];
|
|
ABTExperiment.ttlExpiryEventToLog = experimentPayload[kExperimentPayloadKeyTTLExpiryEventToLog];
|
|
ABTExperiment.overflowPolicy = [experimentPayload[kExperimentPayloadKeyOverflowPolicy] intValue];
|
|
|
|
// Serialize the experiment payload.
|
|
NSData *serializedABTExperiment = ABTExperiment.data;
|
|
return serializedABTExperiment;
|
|
}
|
|
|
|
- (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *)response {
|
|
// cache fetched experiment payloads.
|
|
[_experimentPayloads removeAllObjects];
|
|
[_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload];
|
|
|
|
for (NSDictionary<NSString *, id> *experiment in response) {
|
|
NSData *protoPayload = [self convertABTExperimentPayloadToProto:experiment];
|
|
[_experimentPayloads addObject:protoPayload];
|
|
// We will add the new serialized JSON data to the database.
|
|
// TODO: (b/129272809). Eventually, RC and ABT need to be migrated to move off protos once
|
|
// (most) customers have migrated to using the new SDK (and hence saving the new JSON based
|
|
// payload in the database).
|
|
NSError *error;
|
|
NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment
|
|
options:kNilOptions
|
|
error:&error];
|
|
if (!JSONPayload || error) {
|
|
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030",
|
|
@"Invalid experiment payload to be serialized.");
|
|
}
|
|
|
|
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
|
|
value:JSONPayload
|
|
completionHandler:nil];
|
|
}
|
|
}
|
|
|
|
- (void)updateExperiments {
|
|
FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init];
|
|
|
|
// Get the last experiment start time prior to the latest payload.
|
|
NSTimeInterval lastStartTime =
|
|
[_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
|
|
|
|
// Update the last experiment start time with the latest payload.
|
|
[self updateExperimentStartTime];
|
|
|
|
[self.experimentController
|
|
updateExperimentsWithServiceOrigin:kServiceOrigin
|
|
events:lifecycleEvent
|
|
policy:ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest
|
|
lastStartTime:lastStartTime
|
|
payloads:_experimentPayloads];
|
|
}
|
|
|
|
- (void)updateExperimentStartTime {
|
|
NSTimeInterval existingLastStartTime =
|
|
[_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
|
|
|
|
NSTimeInterval latestStartTime =
|
|
[self latestStartTimeWithExistingLastStartTime:existingLastStartTime];
|
|
|
|
_experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime);
|
|
|
|
if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) {
|
|
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
|
|
@"Invalid fetched experiment metadata to be serialized.");
|
|
return;
|
|
}
|
|
NSError *error;
|
|
NSData *serializedExperimentMetadata =
|
|
[NSJSONSerialization dataWithJSONObject:_experimentMetadata
|
|
options:NSJSONWritingPrettyPrinted
|
|
error:&error];
|
|
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
|
|
value:serializedExperimentMetadata
|
|
completionHandler:nil];
|
|
}
|
|
|
|
- (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime {
|
|
return [self.experimentController
|
|
latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime
|
|
andPayloads:_experimentPayloads];
|
|
}
|
|
@end
|