Skip to content

Commit

Permalink
feat: add disabled exposure getParamStore func (#282)
Browse files Browse the repository at this point in the history
Add ability to check a param store without logging any exposures.

Tested by Kong: statsig-io/kong#2225
<img width="737" alt="Screenshot 2024-06-13 at 1 45 21 PM"
src="https://github.com/statsig-io/ios-client-sdk/assets/95646168/98b69030-6272-4287-8c8d-1c7aa7180ecb">
  • Loading branch information
daniel-statsig authored Jun 13, 2024
1 parent c54eea3 commit 1f5be3c
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 3 deletions.
36 changes: 35 additions & 1 deletion Sources/Statsig/InternalStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/Statsig/Layer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
190 changes: 190 additions & 0 deletions Sources/Statsig/ParameterStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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 var shouldExpose = true

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 = shouldExpose
? client.getFeatureGate(gateName)
: client.getFeatureGateWithExposureLoggingDisabled(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 = shouldExpose
? client.getConfig(configName)
: client.getConfigWithExposureLoggingDisabled(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 = shouldExpose
? client.getExperiment(experimentName)
: client.getExperimentWithExposureLoggingDisabled(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 = shouldExpose
? client.getLayer(layerName)
: client.getLayerWithExposureLoggingDisabled(layerName)
return layer.getValue(forKey: paramName, defaultValue: defaultValue)
}

}
50 changes: 49 additions & 1 deletion Sources/Statsig/Statsig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,29 @@ 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 {
return getParameterStoreImpl(
storeName,
withExposures: true,
functionName: funcName()
)
}

public static func getParameterStoreWithExposureLoggingDisabled(
_ storeName: String
) -> ParameterStore {
return getParameterStoreImpl(
storeName,
withExposures: false,
functionName: funcName()
)
}

/**
Logs an exposure event for the given layer parameter. Only required if a related getLayerWithExposureLoggingDisabled call has been made.
Expand Down Expand Up @@ -588,7 +611,12 @@ public class Statsig {
return result
}

private static func getLayerImpl(_ layerName: String, keepDeviceValue: Bool, withExposures: Bool, functionName: String) -> Layer {
private static func getLayerImpl(
_ layerName: String,
keepDeviceValue: Bool,
withExposures: Bool,
functionName: String
) -> Layer {
var result: Layer = Layer(client: nil, name: layerName, evalDetails: .uninitialized())
errorBoundary.capture(functionName) {
guard let client = client else {
Expand All @@ -602,6 +630,26 @@ public class Statsig {
}
return result
}

private static func getParameterStoreImpl(
_ storeName: String,
withExposures: Bool,
functionName: String
) -> ParameterStore {
var result: ParameterStore? = nil
errorBoundary.capture(functionName) {
guard let client = client else {
print("[Statsig]: \(getUnstartedErrorMessage(functionName)). Returning a dummy ParameterStore that will only return default values.")
return
}

result = withExposures ? client.getParameterStore(storeName) : client.getParameterStoreWithExposureLoggingDisabled(storeName)
}
return result ?? ParameterStore(
name: storeName,
evaluationDetails: .uninitialized()
)
}

private static func getEmptyConfig(_ name: String) -> DynamicConfig {
return DynamicConfig(configName: name, evalDetails: .uninitialized())
Expand Down
27 changes: 27 additions & 0 deletions Sources/Statsig/StatsigClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,33 @@ extension StatsigClient {
}
}

// MARK: Parameter Stores

extension StatsigClient {
public func getParameterStore(_ storeName: String) -> ParameterStore {
return getParameterStoreImpl(storeName, shouldExpose: true)
}

public func getParameterStoreWithExposureLoggingDisabled(_ storeName: String) -> ParameterStore {
return getParameterStoreImpl(storeName, shouldExpose: false)
}

private func getParameterStoreImpl(_ storeName: String, shouldExpose: Bool) -> ParameterStore {
logger.incrementNonExposedCheck(storeName)

var store = store.getParamStore(client: self, forName: storeName)

store.shouldExpose = shouldExpose

if let cb = statsigOptions.evaluationCallback {
cb(.parameterStore(store))
}

return store
}

}


// MARK: Log Event
extension StatsigClient {
Expand Down
Loading

0 comments on commit 1f5be3c

Please sign in to comment.