Skip to content

Commit

Permalink
[Config] Port 'ConfigExperiment' to Swift
Browse files Browse the repository at this point in the history
  • Loading branch information
ncooke3 committed Dec 3, 2024
1 parent 4c1ca6d commit 4795a4c
Showing 1 changed file with 192 additions and 0 deletions.
192 changes: 192 additions & 0 deletions FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift
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

View workflow job for this annotation

GitHub Actions / sample-build-test

value of type 'ExperimentController' has no member 'updateExperiments'

Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

value of type 'ExperimentController' has no member 'updateExperiments'

Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

value of type 'ExperimentController' has no member 'updateExperiments'

Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

value of type 'ExperimentController' has no member 'updateExperiments'

Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

value of type 'ExperimentController' has no member 'updateExperiments'

Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

value of type 'ExperimentController' has no member 'updateExperiments'

Check failure on line 133 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / installation-test

value of type 'ExperimentController' has no member 'updateExperiments'
withServiceOrigin: Self.serviceOrigin,
events: lifecycleEvent,
policy: .discardOldest,

Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / sample-build-test

cannot infer contextual base in reference to member 'discardOldest'

Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

cannot infer contextual base in reference to member 'discardOldest'

Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

cannot infer contextual base in reference to member 'discardOldest'

Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

cannot infer contextual base in reference to member 'discardOldest'

Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

cannot infer contextual base in reference to member 'discardOldest'

Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

cannot infer contextual base in reference to member 'discardOldest'

Check failure on line 136 in FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift

View workflow job for this annotation

GitHub Actions / installation-test

cannot infer contextual base in reference to member 'discardOldest'
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
)
}
}

0 comments on commit 4795a4c

Please sign in to comment.