Skip to content

Commit

Permalink
feat: bootstrap initial local evaluation flags (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgiori authored Nov 30, 2023
1 parent 3d5db9d commit 7c5c995
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 0 deletions.
18 changes: 18 additions & 0 deletions Sources/Experiment/ExperimentClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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<EvaluationFlag> {
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)
}
}
}
}
20 changes: 20 additions & 0 deletions Sources/Experiment/ExperimentConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions Tests/ExperimentTests/ExperimentClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 7c5c995

Please sign in to comment.