Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bootstrap initial local evaluation flags #41

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading