From 1e7033e0c9916d7acc6b8e1f0755ea5f7e572171 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 23 Nov 2023 11:15:41 -0800 Subject: [PATCH] feat: bootstrap initial local evaluation flags --- Sources/Experiment/ExperimentClient.swift | 18 +++++++++ Sources/Experiment/ExperimentConfig.swift | 20 ++++++++++ .../ExperimentClientTests.swift | 38 +++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/Sources/Experiment/ExperimentClient.swift b/Sources/Experiment/ExperimentClient.swift index 29713e2..f16d8f7 100644 --- a/Sources/Experiment/ExperimentClient.swift +++ b/Sources/Experiment/ExperimentClient.swift @@ -90,6 +90,7 @@ internal class DefaultExperimentClient : NSObject, ExperimentClient { self.variants.load() self.flags = getFlagStorage(apiKey: self.apiKey, instanceName: self.config.instanceName, storage: storage) self.flags.load() + self.flags.mergeInitialFlagsWithStorage(config.initialFlags) } public func start(_ user: ExperimentUser? = nil, completion: ((Error?) -> Void)? = nil) -> Void { @@ -408,6 +409,7 @@ internal class DefaultExperimentClient : NSObject, ExperimentClient { self.flags.clear() self.flags.putAll(values: flags) self.flags.store() + self.flags.mergeInitialFlagsWithStorage(self.config.initialFlags) } completion?(nil) case .failure(let error): @@ -718,3 +720,19 @@ internal extension EvaluationVariant { return Variant(self.value as? String, payload: self.payload, expKey: experimentKey, key: self.key, metadata: metadata) } } + +private extension LoadStoreCache { + func mergeInitialFlagsWithStorage(_ initialFlagsString: String?) { + guard let initialFlagsData = initialFlagsString?.data(using: .utf8) else { + return + } + guard let initialFlags = try? JSONDecoder().decode([EvaluationFlag].self, from: initialFlagsData) else { + return + } + for flag in initialFlags { + if self.get(key: flag.key) == nil { + self.put(key: flag.key, value: flag) + } + } + } +} diff --git a/Sources/Experiment/ExperimentConfig.swift b/Sources/Experiment/ExperimentConfig.swift index 5e0be08..3f9474b 100644 --- a/Sources/Experiment/ExperimentConfig.swift +++ b/Sources/Experiment/ExperimentConfig.swift @@ -22,6 +22,7 @@ import Foundation @objc public let debug: Bool @objc public let instanceName: String @objc public let fallbackVariant: Variant + @objc public let initialFlags: String? @objc public let initialVariants: [String: Variant] @objc public let source: Source @objc public let serverUrl: String @@ -42,6 +43,7 @@ import Foundation self.debug = ExperimentConfig.Defaults.debug self.instanceName = ExperimentConfig.Defaults.instanceName self.fallbackVariant = ExperimentConfig.Defaults.fallbackVariant + self.initialFlags = ExperimentConfig.Defaults.initialFlags self.initialVariants = ExperimentConfig.Defaults.initialVariants self.source = ExperimentConfig.Defaults.source self.serverUrl = ExperimentConfig.Defaults.serverUrl @@ -62,6 +64,7 @@ import Foundation self.debug = builder.debug self.instanceName = builder.instanceName self.fallbackVariant = builder.fallbackVariant + self.initialFlags = builder.initialFlags self.initialVariants = builder.initialVariants self.source = builder.source self.serverUrl = builder.serverUrl @@ -82,6 +85,7 @@ import Foundation self.debug = builder.debug self.instanceName = builder.instanceName self.fallbackVariant = builder.fallbackVariant + self.initialFlags = builder.initialFlags self.initialVariants = builder.initialVariants self.source = builder.source self.serverUrl = builder.serverUrl @@ -102,6 +106,7 @@ import Foundation static let debug: Bool = false static let instanceName: String = "$default_instance" static let fallbackVariant: Variant = Variant() + static let initialFlags: String? = nil static let initialVariants: [String: Variant] = [:] static let source: Source = Source.LocalStorage static let serverUrl: String = "https://api.lab.amplitude.com" @@ -124,6 +129,7 @@ import Foundation internal var debug: Bool = ExperimentConfig.Defaults.debug internal var instanceName = ExperimentConfig.Defaults.instanceName internal var fallbackVariant: Variant = ExperimentConfig.Defaults.fallbackVariant + internal var initialFlags: String? = ExperimentConfig.Defaults.initialFlags internal var initialVariants: [String: Variant] = ExperimentConfig.Defaults.initialVariants internal var source: Source = ExperimentConfig.Defaults.source internal var serverUrl: String = ExperimentConfig.Defaults.serverUrl @@ -161,6 +167,12 @@ import Foundation return self } + @discardableResult + public func initialFlags(_ initialFlags: String?) -> Builder { + self.initialFlags = initialFlags + return self + } + @discardableResult public func initialVariants(_ initialVariants: [String: Variant]) -> Builder { self.initialVariants = initialVariants @@ -268,6 +280,7 @@ import Foundation .debug(self.debug) .instanceName(self.instanceName) .fallbackVariant(self.fallbackVariant) + .initialFlags(self.initialFlags) .initialVariants(self.initialVariants) .source(self.source) .serverUrl(self.serverUrl) @@ -293,6 +306,7 @@ import Foundation internal var debug: Bool = ExperimentConfig.Defaults.debug internal var instanceName: String = ExperimentConfig.Defaults.instanceName internal var fallbackVariant: Variant = ExperimentConfig.Defaults.fallbackVariant + internal var initialFlags: String? = ExperimentConfig.Defaults.initialFlags internal var initialVariants: [String: Variant] = ExperimentConfig.Defaults.initialVariants internal var source: Source = ExperimentConfig.Defaults.source internal var serverUrl: String = ExperimentConfig.Defaults.serverUrl @@ -326,6 +340,12 @@ import Foundation return self } + @discardableResult + @objc public func initialFlags(_ initialFlags: String?) -> ExperimentConfigBuilder { + self.initialFlags = initialFlags + return self + } + @discardableResult @objc public func initialVariants(_ initialVariants: [String: Variant]) -> ExperimentConfigBuilder { self.initialVariants = initialVariants diff --git a/Tests/ExperimentTests/ExperimentClientTests.swift b/Tests/ExperimentTests/ExperimentClientTests.swift index 457ba6b..e34bf64 100644 --- a/Tests/ExperimentTests/ExperimentClientTests.swift +++ b/Tests/ExperimentTests/ExperimentClientTests.swift @@ -1149,6 +1149,44 @@ class ExperimentClientTests: XCTestCase { client.fetchBlocking(user: user) XCTAssertEqual(1, client.fetchCalls) } + + func testInitialflags() { + let initialFlags = """ + [ + {"key":"sdk-ci-test-local","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"off"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}}, + {"key":"sdk-ci-test-local-2","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"on"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}} + ] + """ + let storage = InMemoryStorage() + let config = ExperimentConfigBuilder() + .initialFlags(initialFlags) + .build() + let client = DefaultExperimentClient(apiKey: API_KEY, config: config, storage: storage) + let user = ExperimentUserBuilder() + .userId("user_id") + .deviceId("device_id") + .build() + client.setUser(user) + // before start, should get result from initial flags + var variant = client.variant("sdk-ci-test-local") + var variant2 = client.variant("sdk-ci-test-local-2") + XCTAssertEqual("off", variant.key!) + XCTAssertEqual("on", variant2.key!) + client.startBlocking(user: user) + // After start, should get result from downloaded flag, fallback on initial flag + variant = client.variant("sdk-ci-test-local") + variant2 = client.variant("sdk-ci-test-local-2") + XCTAssertEqual("on", variant.key!) + XCTAssertEqual("on", variant2.key!) + // Initialize a second client with the same storage to simulate an app restart + let client2 = DefaultExperimentClient(apiKey: API_KEY, config: config, storage: storage) + client2.setUser(user) + // Should get downloaded flag persisted in storage without calling start + variant = client2.variant("sdk-ci-test-local") + variant2 = client2.variant("sdk-ci-test-local-2") + XCTAssertEqual("on", variant.key!) + XCTAssertEqual("on", variant2.key!) + } } class TestAnalyticsProvider : ExperimentAnalyticsProvider {