Skip to content

Commit 6d667b7

Browse files
committed
[Config] Port 'ConfigExperiment' to Swift
1 parent 2176e99 commit 6d667b7

File tree

1 file changed

+192
-0
lines changed

1 file changed

+192
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseABTesting
16+
import Foundation
17+
18+
/// Handles experiment information update and persistence.
19+
/*@objc(RCNConfigExperiment)*/ open class ConfigExperiment: NSObject {
20+
private static let experimentMetadataKeyLastStartTime = "last_experiment_start_time"
21+
private static let serviceOrigin = "frc"
22+
23+
private var experimentPayloads: [Data]
24+
private var experimentMetadata: [String: Any]?
25+
private var activeExperimentPayloads: [Data]
26+
private let dbManager: ConfigDBManager?
27+
private let experimentController: ExperimentController
28+
private let experimentStartTimeDateFormatter: DateFormatter
29+
30+
/// Designated initializer;
31+
public init(DBManager: ConfigDBManager?,
32+
experimentController controller: ExperimentController?) {
33+
experimentPayloads = []
34+
experimentMetadata = [:]
35+
activeExperimentPayloads = []
36+
experimentStartTimeDateFormatter = {
37+
let dateFormatter = DateFormatter()
38+
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
39+
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
40+
// Locale needs to be hardcoded. See
41+
// https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
42+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
43+
// TODO(ncooke3): Trace back and see why timeZone is set twice.
44+
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
45+
return dateFormatter
46+
}()
47+
dbManager = DBManager
48+
experimentController = controller!
49+
super.init()
50+
loadExperimentFromTable()
51+
}
52+
53+
private func loadExperimentFromTable() {
54+
guard let dbManager else { return }
55+
56+
let completionHandler: (Bool, [String: Sendable]?) -> Void = { [weak self] _, result in
57+
guard let self else { return }
58+
59+
if result?[ConfigConstants.experimentTableKeyPayload] != nil {
60+
self.experimentPayloads.removeAll()
61+
if let experiments = result?[ConfigConstants.experimentTableKeyPayload] as? [Data] {
62+
for experiment in experiments {
63+
do {
64+
try JSONSerialization.jsonObject(with: experiment)
65+
self.experimentPayloads.append(experiment)
66+
} catch {
67+
RCLog.warning("I-RCN000031", "Experiment payload could not be parsed as JSON.")
68+
}
69+
}
70+
}
71+
}
72+
73+
if result?[ConfigConstants.experimentTableKeyMetadata] != nil {
74+
self
75+
.experimentMetadata =
76+
result?[ConfigConstants.experimentTableKeyMetadata] as? [String: Any]
77+
}
78+
79+
if result?[ConfigConstants.experimentTableKeyActivePayload] != nil {
80+
self.activeExperimentPayloads.removeAll()
81+
if let experiments = result?[ConfigConstants.experimentTableKeyActivePayload] as? [Data] {
82+
for experiment in experiments {
83+
do {
84+
try JSONSerialization.jsonObject(with: experiment)
85+
self.activeExperimentPayloads.append(experiment)
86+
} catch {
87+
RCLog.warning(
88+
"I-RCN000031",
89+
"Activated experiment payload could not be parsed as JSON."
90+
)
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
dbManager.loadExperiment(completionHandler: completionHandler)
98+
}
99+
100+
/// Update/Persist experiment information from config fetch response.
101+
open func updateExperiments(withResponse response: [[String: Any]]?) {
102+
// Cache fetched experiment payloads.
103+
experimentPayloads.removeAll()
104+
dbManager?.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyPayload)
105+
106+
if let response {
107+
for experiment in response {
108+
do {
109+
let jsonData = try JSONSerialization.data(withJSONObject: experiment)
110+
experimentPayloads.append(jsonData)
111+
dbManager?
112+
.insertExperimentTable(
113+
withKey: ConfigConstants.experimentTableKeyPayload,
114+
value: jsonData
115+
)
116+
} catch {
117+
RCLog.error("I-RCN000030", "Invalid experiment payload to be serialized.")
118+
}
119+
}
120+
}
121+
}
122+
123+
/// Update experiments to Firebase Analytics when `activateWithCompletion:` happens.
124+
open func updateExperiments(handler: (((any Error)?) -> Void)? = nil) {
125+
let lifecycleEvent = LifecycleEvents()
126+
127+
// Get the last experiment start time prior to the latest payload.
128+
let lastStartTime = experimentMetadata?[Self.experimentMetadataKeyLastStartTime] as? Double
129+
130+
// Update the last experiment start time with the latest payload.
131+
updateExperimentStartTime()
132+
experimentController
133+
.updateExperiments(
134+
withServiceOrigin: Self.serviceOrigin,
135+
events: lifecycleEvent,
136+
policy: .discardOldest,
137+
lastStartTime: lastStartTime,
138+
payloads: experimentPayloads,
139+
completionHandler: handler
140+
)
141+
142+
// Update activated experiments payload and metadata in DB.
143+
updateActiveExperimentsInDB()
144+
}
145+
146+
private func updateExperimentStartTime() {
147+
let existingLastStartTime =
148+
experimentMetadata?[Self.experimentMetadataKeyLastStartTime] as? Double
149+
150+
let latestStartTime = latestStartTime(existingLastStartTime: existingLastStartTime ?? 0)
151+
152+
experimentMetadata?[Self.experimentMetadataKeyLastStartTime] = latestStartTime
153+
154+
guard let experimentMetadata, JSONSerialization.isValidJSONObject(experimentMetadata) else {
155+
RCLog.error("I-RCN000028", "Invalid fetched experiment metadata to be serialized.")
156+
return
157+
}
158+
159+
if let serializedExperimentMetadata = try? JSONSerialization.data(
160+
withJSONObject: experimentMetadata,
161+
options: .prettyPrinted
162+
) {
163+
dbManager?
164+
.insertExperimentTable(
165+
withKey: ConfigConstants.experimentTableKeyMetadata,
166+
value: serializedExperimentMetadata
167+
)
168+
}
169+
}
170+
171+
private func updateActiveExperimentsInDB() {
172+
// Put current fetched experiment payloads into activated experiment DB.
173+
activeExperimentPayloads.removeAll()
174+
dbManager?.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyActivePayload)
175+
for data in experimentPayloads {
176+
activeExperimentPayloads.append(data)
177+
dbManager?
178+
.insertExperimentTable(
179+
withKey: ConfigConstants.experimentTableKeyActivePayload,
180+
value: data
181+
)
182+
}
183+
}
184+
185+
private func latestStartTime(existingLastStartTime: Double) -> TimeInterval {
186+
experimentController
187+
.latestExperimentStartTimestampBetweenTimestamp(
188+
existingLastStartTime,
189+
andPayloads: experimentPayloads
190+
)
191+
}
192+
}

0 commit comments

Comments
 (0)