From 46377c7c9e92d68358bdb8f1c03a413a3517feda Mon Sep 17 00:00:00 2001 From: Daniel <95646168+daniel-statsig@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:32:46 -0700 Subject: [PATCH] feat: add ability to use cache over bootstrap (#56) * chore: address feedback * feat: add ability to use cache over bootstrap * test: verify cache is used instead of bootstrap --- .../StatsigOnDeviceEvaluations/Statsig.swift | 18 +++-- .../StatsigOptions.swift | 7 +- .../SynchronousInitTest.swift | 77 +++++++++++++++++++ Tests/TestUtils/Resources/EmptyDcs.json | 20 +++++ 4 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 Tests/StatsigOnDeviceEvaluationsTestsSwift/SynchronousInitTest.swift create mode 100644 Tests/TestUtils/Resources/EmptyDcs.json diff --git a/Sources/StatsigOnDeviceEvaluations/Statsig.swift b/Sources/StatsigOnDeviceEvaluations/Statsig.swift index 18fa889..f0871bb 100644 --- a/Sources/StatsigOnDeviceEvaluations/Statsig.swift +++ b/Sources/StatsigOnDeviceEvaluations/Statsig.swift @@ -443,15 +443,23 @@ extension Statsig { _ context: StatsigContext, _ value: SynchronousSpecsValue ) -> Error? { - let (result, error) = parseSpecsValue(value) + let (bootstrap, error) = parseSpecsValue(value) - guard error == nil, let result = result else { + guard error == nil, let bootstrap = bootstrap else { return error ?? StatsigError.invalidSynchronousSpecs } - + + if context.options?.useNewerCacheValuesOverProvidedValues == true { + context.store.loadFromCache(context.sdkKey) + + if bootstrap.response.time < context.store.getSourceInfo().lcut { + return nil + } + } + context.store.setAndCacheValues( - response: result.response, - responseData: result.raw, + response: bootstrap.response, + responseData: bootstrap.raw, sdkKey: context.sdkKey, source: .bootstrap ) diff --git a/Sources/StatsigOnDeviceEvaluations/StatsigOptions.swift b/Sources/StatsigOnDeviceEvaluations/StatsigOptions.swift index 9e10e58..df33bc8 100644 --- a/Sources/StatsigOnDeviceEvaluations/StatsigOptions.swift +++ b/Sources/StatsigOnDeviceEvaluations/StatsigOptions.swift @@ -55,7 +55,12 @@ import Foundation * Plugin to override SDK evaluations */ @objc public var overrideAdapter: OverrideAdapter? - + + /** + * When bootstrapping (initializeSync or updateSync), set this flag if you would like to use cache values if they are "fresher" then the bootstrap values + */ + @objc public var useNewerCacheValuesOverProvidedValues: Bool = false + public override init() { environment = StatsigEnvironment() } diff --git a/Tests/StatsigOnDeviceEvaluationsTestsSwift/SynchronousInitTest.swift b/Tests/StatsigOnDeviceEvaluationsTestsSwift/SynchronousInitTest.swift new file mode 100644 index 0000000..4921d9e --- /dev/null +++ b/Tests/StatsigOnDeviceEvaluationsTestsSwift/SynchronousInitTest.swift @@ -0,0 +1,77 @@ +import Quick +import Nimble +import XCTest +import StatsigTestUtils + +@testable import StatsigOnDeviceEvaluations + +func getTestDcsWithTimeField(_ res: String, _ time: Int) -> NSString { + var json = TestResources.getJson(res) + json["time"] = time + return String(data: try! JSONSerialization.data(withJSONObject: json), encoding: .utf8)! as NSString +} + +final class SynchronousInitTest: QuickSpec { + override class func spec() { + describe("SynchronousInit") { + let user = StatsigUser(userID: "a-user") + + func primeCache() { + NetworkStubs.clearAllStubs() + + NetworkStubs.stubEndpoint( + endpoint: "download_config_specs", + resource: "RulesetsDownloadConfigsSpecs" + ) + + let client = Statsig() + waitUntil { done in + client.initialize("client-key") { _ in done() } + } + + client.shutdown() + + NetworkStubs.clearAllStubs() + } + + it("uses cache if newer and useNewerCacheValuesOverProvidedValues is true") { + primeCache() + + let opts = StatsigOptions() + opts.useNewerCacheValuesOverProvidedValues = true + + let dcs = getTestDcsWithTimeField("EmptyDcs", 123) + let client = Statsig() + let _ = client.initializeSync("client-key", initialSpecs: dcs, options: opts) + + let gate = client.getFeatureGate("test_public", user) + expect(gate.evaluationDetails.reason).to(equal("Cache")) + } + + it("uses bootstrap if useNewerCacheValuesOverProvidedValues is false") { + primeCache() + + let dcs = getTestDcsWithTimeField("RulesetsDownloadConfigsSpecs", 123) + let client = Statsig() + let _ = client.initializeSync("client-key", initialSpecs: dcs) + + let gate = client.getFeatureGate("test_public", user) + expect(gate.evaluationDetails.reason).to(equal("Bootstrap")) + } + + it("uses bootstrap if newer") { + primeCache() + + let opts = StatsigOptions() + opts.useNewerCacheValuesOverProvidedValues = true + + let dcs = getTestDcsWithTimeField("RulesetsDownloadConfigsSpecs", Int.max) + let client = Statsig() + let _ = client.initializeSync("client-key", initialSpecs: dcs, options: opts) + + let gate = client.getFeatureGate("test_public", user) + expect(gate.evaluationDetails.reason).to(equal("Bootstrap")) + } + } + } +} diff --git a/Tests/TestUtils/Resources/EmptyDcs.json b/Tests/TestUtils/Resources/EmptyDcs.json new file mode 100644 index 0000000..5b62b7c --- /dev/null +++ b/Tests/TestUtils/Resources/EmptyDcs.json @@ -0,0 +1,20 @@ +{ + "dynamic_configs": [], + "feature_gates": [], + "layer_configs": [], + "id_lists": {}, + "layers": {}, + "has_updates": true, + "time": 1697131166636, + "company_id": "5NprLGRxV3W28hreG51Z7n", + "diagnostics": { + "initialize": 1000, + "dcs": 1000, + "download_config_specs": 1000, + "idlist": 100, + "get_id_list": 100, + "get_id_list_sources": 100, + "log": 100, + "log_event": 100 + } +}