Skip to content

Commit

Permalink
feat: add support for getParameterStore (#279)
Browse files Browse the repository at this point in the history
Tested with Kong: statsig-io/kong#2225

<img width="674" alt="Screenshot 2024-06-10 at 12 25 49 PM"
src="https://github.com/statsig-io/ios-client-sdk/assets/95646168/14111aa9-5e9f-4c14-a5de-d4f8648331bf">
  • Loading branch information
daniel-statsig authored Jun 10, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent c0fef62 commit 0a81ded
Showing 8 changed files with 280 additions and 2 deletions.
36 changes: 35 additions & 1 deletion Sources/Statsig/InternalStore.swift
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ struct StatsigValuesCache {
var gates: [String: [String: Any]]? = nil
var configs: [String: [String: Any]]? = nil
var layers: [String: [String: Any]]? = nil
var paramStores: [String: [String: Any]]? = nil
var hashUsed: String? = nil
var sdkKey: String
var options: StatsigOptions
@@ -37,6 +38,7 @@ struct StatsigValuesCache {
gates = userCache[InternalStore.gatesKey] as? [String: [String: Any]]
configs = userCache[InternalStore.configsKey] as? [String: [String: Any]]
layers = userCache[InternalStore.layerConfigsKey] as? [String: [String: Any]]
paramStores = userCache[InternalStore.paramStoresKey] as? [String: [String: Any]]
hashUsed = userCache[InternalStore.hashUsedKey] as? String
}
}
@@ -100,13 +102,33 @@ struct StatsigValuesCache {
return Layer(
client: client,
name: layerName,
configObj: configObj, evalDetails: getEvaluationDetails(.Recognized)
configObj: configObj,
evalDetails: getEvaluationDetails(.Recognized)
)
}

print("[Statsig]: The layer with name \(layerName) does not exist. Returning an empty Layer.")
return createUnfoundLayer(client, layerName)
}

func getParamStore(_ client: StatsigClient?, _ storeName: String) -> ParameterStore {
guard let stores = paramStores else {
print("[Statsig]: Failed to get parameter store with name \(storeName). Returning an empty ParameterStore.")
return createUnfoundParamStore(client, storeName)
}

if let config = stores[storeName] ?? stores[storeName.hashSpecName(hashUsed)] {
return ParameterStore(
name: storeName,
evaluationDetails: getEvaluationDetails(.Recognized),
client: client,
configuration: config
)
}

print("[Statsig]: The parameter store with name \(storeName) does not exist. Returning an empty ParameterStore.")
return createUnfoundParamStore(client, storeName)
}

func getStickyExperiment(_ expName: String) -> [String: Any]? {
let expNameHash = expName.hashSpecName(hashUsed)
@@ -159,6 +181,7 @@ struct StatsigValuesCache {
cache[InternalStore.gatesKey] = values[InternalStore.gatesKey]
cache[InternalStore.configsKey] = values[InternalStore.configsKey]
cache[InternalStore.layerConfigsKey] = values[InternalStore.layerConfigsKey]
cache[InternalStore.paramStoresKey] = values[InternalStore.paramStoresKey]
cache[InternalStore.lcutKey] = Time.parse(values[InternalStore.lcutKey])
cache[InternalStore.evalTimeKey] = Time.now()
cache[InternalStore.userHashKey] = userHash
@@ -336,6 +359,10 @@ struct StatsigValuesCache {
evalDetails: getEvaluationDetails(.Unrecognized)
)
}

private func createUnfoundParamStore(_ client: StatsigClient?, _ name: String) -> ParameterStore {
ParameterStore(name: name, evaluationDetails: getEvaluationDetails(.Unrecognized))
}
}

class InternalStore {
@@ -353,6 +380,7 @@ class InternalStore {
static let configsKey = "dynamic_configs"
static let stickyExpKey = "sticky_experiments"
static let layerConfigsKey = "layer_configs"
static let paramStoresKey = "param_stores"
static let lcutKey = "time"
static let evalTimeKey = "evaluation_time"
static let userHashKey = "user_hash"
@@ -453,6 +481,12 @@ class InternalStore {
)
})
}

func getParamStore(client: StatsigClient?, forName storeName: String) -> ParameterStore {
storeQueue.sync {
return cache.getParamStore(client, storeName)
}
}

func finalizeValues(completion: (() -> Void)? = nil) {
storeQueue.async(flags: .barrier) { [weak self] in
2 changes: 1 addition & 1 deletion Sources/Statsig/Layer.swift
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ public struct Layer: ConfigProtocol {
public let allocatedExperimentName: String

/**
(For debug purposes) Why did Statsig return this DynamicConfig
(For debug purposes) Why did Statsig return this Layer
*/
public let evaluationDetails: EvaluationDetails

180 changes: 180 additions & 0 deletions Sources/Statsig/ParameterStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import Foundation

typealias ParamStoreConfiguration = [String: [String: Any]]

fileprivate struct RefType {
static let staticValue = "static"
static let gate = "gate"
static let dynamicConfig = "dynamic_config"
static let experiment = "experiment"
static let layer = "layer"
}

fileprivate struct ParamKeys {
static let paramType = "param_type"
static let refType = "ref_type"

// Gate
static let gateName = "gate_name"
static let passValue = "pass_value"
static let failValue = "fail_value"

// Static Value
static let value = "value"

// Dynamic Config / Experiment / Layer
static let paramName = "param_name"
static let configName = "config_name"
static let experimentName = "experiment_name"
static let layerName = "layer_name"
}

public struct ParameterStore {
/**
The name used to retrieve this ParameterStore.
*/
public let name: String

/**
(For debug purposes) Why did Statsig return this ParameterStore
*/
public let evaluationDetails: EvaluationDetails

internal let configuration: ParamStoreConfiguration
weak internal var client: StatsigClient?

internal init(
name: String,
evaluationDetails: EvaluationDetails,
client: StatsigClient? = nil,
configuration: [String: Any] = [:]
) {
self.name = name
self.evaluationDetails = evaluationDetails
self.client = client
self.configuration = configuration as? ParamStoreConfiguration ?? ParamStoreConfiguration()
}

/**
Get the value for the given key. If the value cannot be found, or is found to have a different type than the defaultValue, the defaultValue will be returned.
If a valid value is found, a layer exposure event will be fired.

Parameters:
- forKey: The key of parameter being fetched
- defaultValue: The fallback value if the key cannot be found
*/
public func getValue<T: StatsigDynamicConfigValue>(
forKey paramName: String,
defaultValue: T
) -> T {
if configuration.isEmpty {
return defaultValue
}

guard
let client = client,
let param = configuration[paramName],
let refType = param[ParamKeys.refType] as? String,
let paramType = param[ParamKeys.paramType] as? String,
getTypeOf(defaultValue) == paramType
else {
return defaultValue
}

switch refType {
case RefType.staticValue:
return getMappedStaticValue(param, defaultValue)

case RefType.gate:
return getMappedGateValue(client, param, defaultValue)

case RefType.dynamicConfig:
return getMappedDynamicConfigValue(client, param, defaultValue)

case RefType.experiment:
return getMappedExperimentValue(client, param, defaultValue)

case RefType.layer:
return getMappedLayerValue(client, param, defaultValue)

default:
return defaultValue
}
}
}

fileprivate func getMappedStaticValue<T>(
_ param: [String: Any],
_ defaultValue: T
) -> T {
return param[ParamKeys.value] as? T ?? defaultValue
}


fileprivate func getMappedGateValue<T: StatsigDynamicConfigValue>(
_ client: StatsigClient,
_ param: [String: Any],
_ defaultValue: T
) -> T {
guard
let gateName = param[ParamKeys.gateName] as? String,
let passValue = param[ParamKeys.passValue] as? T,
let failValue = param[ParamKeys.failValue] as? T
else {
return defaultValue
}

let gate = client.getFeatureGate(gateName)
return gate.value ? passValue : failValue
}


fileprivate func getMappedDynamicConfigValue<T: StatsigDynamicConfigValue>(
_ client: StatsigClient,
_ param: [String: Any],
_ defaultValue: T
) -> T {
guard
let configName = param[ParamKeys.configName] as? String,
let paramName = param[ParamKeys.paramName] as? String
else {
return defaultValue
}

let config = client.getConfig(configName)
return config.getValue(forKey: paramName, defaultValue: defaultValue)
}


fileprivate func getMappedExperimentValue<T: StatsigDynamicConfigValue>(
_ client: StatsigClient,
_ param: [String: Any],
_ defaultValue: T
) -> T {
guard
let experimentName = param[ParamKeys.experimentName] as? String,
let paramName = param[ParamKeys.paramName] as? String
else {
return defaultValue
}

let experiment = client.getExperiment(experimentName)
return experiment.getValue(forKey: paramName, defaultValue: defaultValue)
}


fileprivate func getMappedLayerValue<T: StatsigDynamicConfigValue>(
_ client: StatsigClient,
_ param: [String: Any],
_ defaultValue: T
) -> T {
guard
let layerName = param[ParamKeys.layerName] as? String,
let paramName = param[ParamKeys.paramName] as? String
else {
return defaultValue
}

let layer = client.getLayer(layerName)
return layer.getValue(forKey: paramName, defaultValue: defaultValue)
}
18 changes: 18 additions & 0 deletions Sources/Statsig/Statsig.swift
Original file line number Diff line number Diff line change
@@ -251,6 +251,24 @@ public class Statsig {
public static func getLayerWithExposureLoggingDisabled(_ layerName: String, keepDeviceValue: Bool = false) -> Layer {
return getLayerImpl(layerName, keepDeviceValue: keepDeviceValue, withExposures: false, functionName: funcName())
}

/**

*/
public static func getParameterStore(_ storeName: String) -> ParameterStore {
let functionName = funcName()
var result = ParameterStore(name: storeName, evaluationDetails: .uninitialized())

errorBoundary.capture(functionName) {
guard let client = client else {
print("[Statsig]: \(getUnstartedErrorMessage(functionName)). Returning a dummy ParameterStore that will only return default values.")
return
}

result = client.getParameterStore(storeName)
}
return result
}

/**
Logs an exposure event for the given layer parameter. Only required if a related getLayerWithExposureLoggingDisabled call has been made.
15 changes: 15 additions & 0 deletions Sources/Statsig/StatsigClient.swift
Original file line number Diff line number Diff line change
@@ -537,6 +537,21 @@ extension StatsigClient {
}
}

// MARK: Parameter Stores

extension StatsigClient {
public func getParameterStore(_ storeName: String) -> ParameterStore {
logger.incrementNonExposedCheck(storeName)

let store = store.getParamStore(client: self, forName: storeName)
if let cb = statsigOptions.evaluationCallback {
cb(.parameterStore(store))
}

return store
}
}


// MARK: Log Event
extension StatsigClient {
28 changes: 28 additions & 0 deletions Sources/Statsig/StatsigDynamicConfigValue.swift
Original file line number Diff line number Diff line change
@@ -17,3 +17,31 @@ extension Int: StatsigDynamicConfigValue {}
extension String: StatsigDynamicConfigValue {}

extension String?: StatsigDynamicConfigValue {}


fileprivate struct TypeString {
static let boolean = "boolean"
static let number = "number"
static let string = "string"
static let array = "array"
static let object = "object"
}

func getTypeOf<T: StatsigDynamicConfigValue>(_ value: T) -> String? {
switch value {
case is Bool:
return TypeString.boolean
case is Int, is Double:
return TypeString.number
case is String, is Optional<String>:
return TypeString.string
case is Array<StatsigDynamicConfigValue>:
return TypeString.array
case is Dictionary<String, StatsigDynamicConfigValue>:
return TypeString.object
default:
return nil
}
}


1 change: 1 addition & 0 deletions Sources/Statsig/StatsigOptions.swift
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ public class StatsigOptions {
case config (DynamicConfig)
case experiment (DynamicConfig)
case layer (Layer)
case parameterStore (ParameterStore)
}

/**
2 changes: 2 additions & 0 deletions Tests/StatsigTests/EvaluationCallbackSpec.swift
Original file line number Diff line number Diff line change
@@ -54,6 +54,8 @@ final class EvaluationCallbackSpec: BaseSpec {
experimentNameResult = exp.name
case .layer(let layer):
layerNameResult = layer.name
case .parameterStore(let _paramStore):
break
}
}

0 comments on commit 0a81ded

Please sign in to comment.