Skip to content

Commit

Permalink
add manual exposure methods (#129)
Browse files Browse the repository at this point in the history
* add manual exposure methods

* update naming
  • Loading branch information
daniel-statsig authored Oct 6, 2022
1 parent fa4ea48 commit d6617b8
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 124 deletions.
39 changes: 20 additions & 19 deletions Sources/Statsig/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Event {
let secondaryExposures: [[String: String]]?
var statsigMetadata: [String: String]?
var allocatedExperimentHash: String?
var isManualExposure: Bool = false

static let statsigPrefix = "statsig::"
static let configExposureEventName = "config_exposure"
Expand Down Expand Up @@ -42,6 +43,11 @@ class Event {
}
}

func withManualExposureFlag(_ isManualExposure: Bool) -> Event {
self.isManualExposure = isManualExposure
return self
}

static func statsigInternalEvent(
user: StatsigUser,
name: String,
Expand Down Expand Up @@ -138,26 +144,21 @@ class Event {
}

func toDictionary() -> [String: Any] {
var dict = [String: Any]()
dict["eventName"] = name
dict["user"] = user.toDictionary(forLogging: true)
dict["time"] = time
if let value = value {
dict["value"] = value
}
if let metadata = metadata {
dict["metadata"] = metadata
}
if let statsigMetadata = statsigMetadata {
dict["statsigMetadata"] = statsigMetadata
}
if let secondaryExposures = secondaryExposures {
dict["secondaryExposures"] = secondaryExposures
}
if let allocatedExperimentHash = allocatedExperimentHash {
dict["allocatedExperimentHash"] = allocatedExperimentHash
var metadataForLogging = metadata
if isManualExposure {
metadataForLogging = metadataForLogging ?? [:]
metadataForLogging?["isManualExposure"] = "true"
}

return dict
return [
"eventName": name,
"user": user.toDictionary(forLogging: true),
"time": time,
"value": value,
"metadata": metadataForLogging,
"statsigMetadata": statsigMetadata,
"secondaryExposures": secondaryExposures,
"allocatedExperimentHash": allocatedExperimentHash,
].compactMapValues { $0 }
}
}
2 changes: 1 addition & 1 deletion Sources/Statsig/Layer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public struct Layer: ConfigProtocol {
print("[Statsig]: \(forKey) does not exist in this Layer. Returning the defaultValue.")
}
} else {
client?.logLayerParameterExposure(layer: self, parameterName: forKey)
client?.logLayerParameterExposureForLayer(self, parameterName: forKey, isManualExposure: false)
}
return typedResult ?? defaultValue
}
Expand Down
44 changes: 44 additions & 0 deletions Sources/Statsig/Statsig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ public class Statsig {
return checkGateImpl(gateName, withExposures: false, functionName: #function)
}

public static func manuallyLogGateExposure(_ gateName: String) {
errorBoundary.capture {
guard let client = client else {
print("[Statsig]: Must start Statsig first and wait for it to complete before calling manuallyLogGateExposure.")
return
}

client.logGateExposure(gateName)
}
}

public static func getExperiment(_ experimentName: String, keepDeviceValue: Bool = false) -> DynamicConfig {
return getExperimentImpl(experimentName, keepDeviceValue: keepDeviceValue, withExposures: true, functionName: #function)
}
Expand All @@ -68,6 +79,17 @@ public class Statsig {
return getExperimentImpl(experimentName, keepDeviceValue: keepDeviceValue, withExposures: false, functionName: #function)
}

public static func manuallyLogExperimentExposure(_ experimentName: String, keepDeviceValue: Bool = false) {
errorBoundary.capture {
guard let client = client else {
print("[Statsig]: Must start Statsig first and wait for it to complete before calling manuallyLogExperimentExposure.")
return
}

client.logExperimentExposure(experimentName, keepDeviceValue: keepDeviceValue)
}
}

public static func getConfig(_ configName: String) -> DynamicConfig {
return getConfigImpl(configName, withExposures: true, functionName: #function)
}
Expand All @@ -76,6 +98,17 @@ public class Statsig {
return getConfigImpl(configName, withExposures: false, functionName: #function)
}

public static func manuallyLogConfigExposure(_ configName: String) {
errorBoundary.capture {
guard let client = client else {
print("[Statsig]: Must start Statsig first and wait for it to complete before calling manuallyLogConfigExposure.")
return
}

client.logConfigExposure(configName)
}
}

public static func getLayer(_ layerName: String, keepDeviceValue: Bool = false) -> Layer {
return getLayerImpl(layerName, keepDeviceValue: keepDeviceValue, withExposures: true, functionName: #function)
}
Expand All @@ -84,6 +117,17 @@ public class Statsig {
return getLayerImpl(layerName, keepDeviceValue: keepDeviceValue, withExposures: false, functionName: #function)
}

public static func manuallyLogLayerParameterExposure(_ layerName: String, _ parameterName: String, keepDeviceValue: Bool = false) {
errorBoundary.capture {
guard let client = client else {
print("[Statsig]: Must start Statsig first and wait for it to complete before calling manuallyLogLayerParameterExposure.")
return
}

client.logLayerParameterExposure(layerName, parameterName: parameterName, keepDeviceValue: keepDeviceValue)
}
}

public static func logEvent(_ withName: String, metadata: [String: String]? = nil) {
logEventImpl(withName, value: nil, metadata: metadata)
}
Expand Down
133 changes: 77 additions & 56 deletions Sources/Statsig/StatsigClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,19 @@ internal class StatsigClient {
internal func checkGate(_ gateName: String) -> Bool {
let gate = store.checkGate(forName: gateName)

logGateExposure(gateName, gate: gate)

return gate.value
}

internal func logGateExposure(_ gateName: String, gate: FeatureGate? = nil) {
let isManualExposure = gate == nil
let gate = gate ?? store.checkGate(forName: gateName)
let gateValue = gate.value
let ruleID = gate.ruleID
let dedupeKey = gateName + (gateValue ? "true" : "false") + ruleID + gate.evaluationDetails.reason.rawValue


if shouldLogExposure(key: dedupeKey) {
logger.log(
Event.gateExposure(
Expand All @@ -68,10 +77,9 @@ internal class StatsigClient {
ruleID: ruleID,
secondaryExposures: gate.secondaryExposures,
evalDetails: gate.evaluationDetails,
disableCurrentVCLogging: statsigOptions.disableCurrentVCLogging))
disableCurrentVCLogging: statsigOptions.disableCurrentVCLogging)
.withManualExposureFlag(isManualExposure))
}

return gate.value
}

internal func checkGateWithExposureLoggingDisabled(_ gateName: String) -> Bool {
Expand All @@ -81,18 +89,7 @@ internal class StatsigClient {
internal func getExperiment(_ experimentName: String, keepDeviceValue: Bool = false) -> DynamicConfig {
let experiment = store.getExperiment(forName: experimentName, keepDeviceValue: keepDeviceValue)

let ruleID = experiment.ruleID
let dedupeKey = experimentName + ruleID + experiment.evaluationDetails.reason.rawValue
if shouldLogExposure(key: dedupeKey) {
logger.log(
Event.configExposure(
user: currentUser,
configName: experimentName,
ruleID: ruleID,
secondaryExposures: experiment.secondaryExposures,
evalDetails: experiment.evaluationDetails,
disableCurrentVCLogging: statsigOptions.disableCurrentVCLogging))
}
logExperimentExposure(experimentName, keepDeviceValue: keepDeviceValue, experiment: experiment)

return experiment
}
Expand All @@ -101,9 +98,31 @@ internal class StatsigClient {
return store.getExperiment(forName: experimentName, keepDeviceValue: keepDeviceValue)
}

internal func logExperimentExposure(_ experimentName: String, keepDeviceValue: Bool, experiment: DynamicConfig? = nil) {
let isManualExposure = experiment == nil
let experiment = experiment ?? store.getExperiment(forName: experimentName, keepDeviceValue: keepDeviceValue)
logConfigExposureForConfig(experimentName, config: experiment, isManualExposure: isManualExposure)
}

internal func getConfig(_ configName: String) -> DynamicConfig {
let config = store.getConfig(forName: configName)

logConfigExposure(configName, config: config)

return config
}

internal func getConfigWithExposureLoggingDisabled(_ configName: String) -> DynamicConfig {
return store.getConfig(forName: configName)
}

internal func logConfigExposure(_ configName: String, config: DynamicConfig? = nil) {
let isManualExposure = config == nil
let config = config ?? store.getConfig(forName: configName)
logConfigExposureForConfig(configName, config: config, isManualExposure: isManualExposure)
}

internal func logConfigExposureForConfig(_ configName: String, config: DynamicConfig, isManualExposure: Bool) {
let ruleID = config.ruleID
let dedupeKey = configName + ruleID + config.evaluationDetails.reason.rawValue
if shouldLogExposure(key: dedupeKey) {
Expand All @@ -114,14 +133,9 @@ internal class StatsigClient {
ruleID: config.ruleID,
secondaryExposures: config.secondaryExposures,
evalDetails: config.evaluationDetails,
disableCurrentVCLogging: statsigOptions.disableCurrentVCLogging))
disableCurrentVCLogging: statsigOptions.disableCurrentVCLogging)
.withManualExposureFlag(isManualExposure))
}

return config
}

internal func getConfigWithExposureLoggingDisabled(_ configName: String) -> DynamicConfig {
return store.getConfig(forName: configName)
}

internal func getLayer(_ layerName: String, keepDeviceValue: Bool = false) -> Layer {
Expand All @@ -132,6 +146,47 @@ internal class StatsigClient {
return store.getLayer(client: nil, forName: layerName, keepDeviceValue: keepDeviceValue)
}


internal func logLayerParameterExposure(_ layerName: String, parameterName: String, keepDeviceValue: Bool) {
let layer = getLayer(layerName, keepDeviceValue: keepDeviceValue)
logLayerParameterExposureForLayer(layer, parameterName: parameterName, isManualExposure: true)
}

internal func logLayerParameterExposureForLayer(_ layer: Layer, parameterName: String, isManualExposure: Bool) {
var exposures = layer.undelegatedSecondaryExposures
var allocatedExperiment = ""
let isExplicit = layer.explicitParameters.contains(parameterName)
if isExplicit {
exposures = layer.secondaryExposures
allocatedExperiment = layer.allocatedExperimentName
}

let dedupeKey = [
layer.name,
layer.ruleID,
allocatedExperiment,
parameterName,
"\(isExplicit)",
layer.evaluationDetails.reason.rawValue
].joined(separator: "|")

if shouldLogExposure(key: dedupeKey) {
logger.log(
Event.layerExposure(
user: currentUser,
configName: layer.name,
ruleID: layer.ruleID,
secondaryExposures: exposures,
disableCurrentVCLogging: statsigOptions.disableCurrentVCLogging,
allocatedExperimentName: allocatedExperiment,
parameterName: parameterName,
isExplicitParameter: isExplicit,
evalDetails: layer.evaluationDetails
)
.withManualExposureFlag(isManualExposure))
}
}

internal func updateUser(_ user: StatsigUser, completion: completionBlock = nil) {
exposureDedupeQueue.async(flags: .barrier) { [weak self] in
self?.loggedExposures.removeAll()
Expand Down Expand Up @@ -205,40 +260,6 @@ internal class StatsigClient {
)
}

internal func logLayerParameterExposure(layer: Layer, parameterName: String) {
var exposures = layer.undelegatedSecondaryExposures
var allocatedExperiment = ""
let isExplicit = layer.explicitParameters.contains(parameterName)
if isExplicit {
exposures = layer.secondaryExposures
allocatedExperiment = layer.allocatedExperimentName
}

let dedupeKey = [
layer.name,
layer.ruleID,
allocatedExperiment,
parameterName,
"\(isExplicit)",
layer.evaluationDetails.reason.rawValue
].joined(separator: "|")

if shouldLogExposure(key: dedupeKey) {
logger.log(
Event.layerExposure(
user: currentUser,
configName: layer.name,
ruleID: layer.ruleID,
secondaryExposures: exposures,
disableCurrentVCLogging: statsigOptions.disableCurrentVCLogging,
allocatedExperimentName: allocatedExperiment,
parameterName: parameterName,
isExplicitParameter: isExplicit,
evalDetails: layer.evaluationDetails
))
}
}

private func fetchAndScheduleSyncing(completion: completionBlock) {
syncTimer?.invalidate()

Expand Down
Loading

0 comments on commit d6617b8

Please sign in to comment.