-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// 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 FirebaseABTesting | ||
import Foundation | ||
|
||
/// Handles experiment information update and persistence. | ||
/*@objc(RCNConfigExperiment)*/ open class ConfigExperiment: NSObject { | ||
private static let experimentMetadataKeyLastStartTime = "last_experiment_start_time" | ||
private static let serviceOrigin = "frc" | ||
|
||
private var experimentPayloads: [Data] | ||
private var experimentMetadata: [String: Any]? | ||
private var activeExperimentPayloads: [Data] | ||
private let dbManager: ConfigDBManager? | ||
private let experimentController: ExperimentController | ||
private let experimentStartTimeDateFormatter: DateFormatter | ||
|
||
/// Designated initializer; | ||
public init(DBManager: ConfigDBManager?, | ||
experimentController controller: ExperimentController?) { | ||
experimentPayloads = [] | ||
experimentMetadata = [:] | ||
activeExperimentPayloads = [] | ||
experimentStartTimeDateFormatter = { | ||
let dateFormatter = DateFormatter() | ||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" | ||
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) | ||
// Locale needs to be hardcoded. See | ||
// https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details. | ||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") | ||
// TODO(ncooke3): Trace back and see why timeZone is set twice. | ||
dateFormatter.timeZone = TimeZone(abbreviation: "UTC") | ||
return dateFormatter | ||
}() | ||
dbManager = DBManager | ||
experimentController = controller! | ||
super.init() | ||
loadExperimentFromTable() | ||
} | ||
|
||
private func loadExperimentFromTable() { | ||
guard let dbManager else { return } | ||
|
||
let completionHandler: (Bool, [String: Sendable]?) -> Void = { [weak self] _, result in | ||
guard let self else { return } | ||
|
||
if result?[ConfigConstants.experimentTableKeyPayload] != nil { | ||
self.experimentPayloads.removeAll() | ||
if let experiments = result?[ConfigConstants.experimentTableKeyPayload] as? [Data] { | ||
for experiment in experiments { | ||
do { | ||
try JSONSerialization.jsonObject(with: experiment) | ||
self.experimentPayloads.append(experiment) | ||
} catch { | ||
RCLog.warning("I-RCN000031", "Experiment payload could not be parsed as JSON.") | ||
} | ||
} | ||
} | ||
} | ||
|
||
if result?[ConfigConstants.experimentTableKeyMetadata] != nil { | ||
self | ||
.experimentMetadata = | ||
result?[ConfigConstants.experimentTableKeyMetadata] as? [String: Any] | ||
} | ||
|
||
if result?[ConfigConstants.experimentTableKeyActivePayload] != nil { | ||
self.activeExperimentPayloads.removeAll() | ||
if let experiments = result?[ConfigConstants.experimentTableKeyActivePayload] as? [Data] { | ||
for experiment in experiments { | ||
do { | ||
try JSONSerialization.jsonObject(with: experiment) | ||
self.activeExperimentPayloads.append(experiment) | ||
} catch { | ||
RCLog.warning( | ||
"I-RCN000031", | ||
"Activated experiment payload could not be parsed as JSON." | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
dbManager.loadExperiment(completionHandler: completionHandler) | ||
} | ||
|
||
/// Update/Persist experiment information from config fetch response. | ||
open func updateExperiments(withResponse response: [[String: Any]]?) { | ||
// Cache fetched experiment payloads. | ||
experimentPayloads.removeAll() | ||
dbManager?.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyPayload) | ||
|
||
if let response { | ||
for experiment in response { | ||
do { | ||
let jsonData = try JSONSerialization.data(withJSONObject: experiment) | ||
experimentPayloads.append(jsonData) | ||
dbManager? | ||
.insertExperimentTable( | ||
withKey: ConfigConstants.experimentTableKeyPayload, | ||
value: jsonData | ||
) | ||
} catch { | ||
RCLog.error("I-RCN000030", "Invalid experiment payload to be serialized.") | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Update experiments to Firebase Analytics when `activateWithCompletion:` happens. | ||
open func updateExperiments(handler: (((any Error)?) -> Void)? = nil) { | ||
let lifecycleEvent = LifecycleEvents() | ||
|
||
// Get the last experiment start time prior to the latest payload. | ||
let lastStartTime = experimentMetadata?[Self.experimentMetadataKeyLastStartTime] as? Double | ||
|
||
// Update the last experiment start time with the latest payload. | ||
updateExperimentStartTime() | ||
experimentController | ||
.updateExperiments( | ||
Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / sample-build-test
Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
|
||
withServiceOrigin: Self.serviceOrigin, | ||
events: lifecycleEvent, | ||
policy: .discardOldest, | ||
Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / sample-build-test
Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift GitHub Actions / remoteconfig (iOS)
|
||
lastStartTime: lastStartTime, | ||
payloads: experimentPayloads, | ||
completionHandler: handler | ||
) | ||
|
||
// Update activated experiments payload and metadata in DB. | ||
updateActiveExperimentsInDB() | ||
} | ||
|
||
private func updateExperimentStartTime() { | ||
let existingLastStartTime = | ||
experimentMetadata?[Self.experimentMetadataKeyLastStartTime] as? Double | ||
|
||
let latestStartTime = latestStartTime(existingLastStartTime: existingLastStartTime ?? 0) | ||
|
||
experimentMetadata?[Self.experimentMetadataKeyLastStartTime] = latestStartTime | ||
|
||
guard let experimentMetadata, JSONSerialization.isValidJSONObject(experimentMetadata) else { | ||
RCLog.error("I-RCN000028", "Invalid fetched experiment metadata to be serialized.") | ||
return | ||
} | ||
|
||
if let serializedExperimentMetadata = try? JSONSerialization.data( | ||
withJSONObject: experimentMetadata, | ||
options: .prettyPrinted | ||
) { | ||
dbManager? | ||
.insertExperimentTable( | ||
withKey: ConfigConstants.experimentTableKeyMetadata, | ||
value: serializedExperimentMetadata | ||
) | ||
} | ||
} | ||
|
||
private func updateActiveExperimentsInDB() { | ||
// Put current fetched experiment payloads into activated experiment DB. | ||
activeExperimentPayloads.removeAll() | ||
dbManager?.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyActivePayload) | ||
for data in experimentPayloads { | ||
activeExperimentPayloads.append(data) | ||
dbManager? | ||
.insertExperimentTable( | ||
withKey: ConfigConstants.experimentTableKeyActivePayload, | ||
value: data | ||
) | ||
} | ||
} | ||
|
||
private func latestStartTime(existingLastStartTime: Double) -> TimeInterval { | ||
experimentController | ||
.latestExperimentStartTimestampBetweenTimestamp( | ||
existingLastStartTime, | ||
andPayloads: experimentPayloads | ||
) | ||
} | ||
} |