Skip to content

Commit

Permalink
evaluation callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
kenny-statsig committed Nov 16, 2023
1 parent e767267 commit 6559260
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 79 deletions.
76 changes: 56 additions & 20 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ func NewClientWithOptions(sdkKey string, options *Options) *Client {

// Checks the value of a Feature Gate for the given user
func (c *Client) CheckGate(user User, gate string) bool {
options := checkGateOptions{logExposure: true}
options := checkGateOptions{disableLogExposures: false}
return c.checkGateImpl(user, gate, options)
}

// Checks the value of a Feature Gate for the given user without logging an exposure event
func (c *Client) CheckGateWithExposureLoggingDisabled(user User, gate string) bool {
options := checkGateOptions{logExposure: false}
options := checkGateOptions{disableLogExposures: true}
return c.checkGateImpl(user, gate, options)
}

Expand All @@ -78,14 +78,16 @@ func (c *Client) ManuallyLogGateExposure(user User, gate string) {

// Gets the DynamicConfig value for the given user
func (c *Client) GetConfig(user User, config string) DynamicConfig {
options := getConfigOptions{logExposure: true}
return c.getConfigImpl(user, config, options)
options := &getConfigOptions{disableLogExposures: false}
context := getConfigImplContext{configOptions: options}
return c.getConfigImpl(user, config, context)
}

// Gets the DynamicConfig value for the given user without logging an exposure event
func (c *Client) GetConfigWithExposureLoggingDisabled(user User, config string) DynamicConfig {
options := getConfigOptions{logExposure: false}
return c.getConfigImpl(user, config, options)
options := &getConfigOptions{disableLogExposures: true}
context := getConfigImplContext{configOptions: options}
return c.getConfigImpl(user, config, context)
}

// Logs an exposure event for the config
Expand All @@ -106,15 +108,19 @@ func (c *Client) GetExperiment(user User, experiment string) DynamicConfig {
if !c.verifyUser(user) {
return *NewConfig(experiment, nil, "", "")
}
return c.GetConfig(user, experiment)
options := &getExperimentOptions{disableLogExposures: false}
context := getConfigImplContext{experimentOptions: options}
return c.getConfigImpl(user, experiment, context)
}

// Gets the DynamicConfig value of an Experiment for the given user without logging an exposure event
func (c *Client) GetExperimentWithExposureLoggingDisabled(user User, experiment string) DynamicConfig {
if !c.verifyUser(user) {
return *NewConfig(experiment, nil, "", "")
}
return c.GetConfigWithExposureLoggingDisabled(user, experiment)
options := &getExperimentOptions{disableLogExposures: true}
context := getConfigImplContext{experimentOptions: options}
return c.getConfigImpl(user, experiment, context)
}

// Logs an exposure event for the experiment
Expand All @@ -124,13 +130,13 @@ func (c *Client) ManuallyLogExperimentExposure(user User, experiment string) {

// Gets the Layer object for the given user
func (c *Client) GetLayer(user User, layer string) Layer {
options := getLayerOptions{logExposure: true}
options := getLayerOptions{disableLogExposures: false}
return c.getLayerImpl(user, layer, options)
}

// Gets the Layer object for the given user without logging an exposure event
func (c *Client) GetLayerWithExposureLoggingDisabled(user User, layer string) Layer {
options := getLayerOptions{logExposure: false}
options := getLayerOptions{disableLogExposures: true}
return c.getLayerImpl(user, layer, options)
}

Expand Down Expand Up @@ -225,15 +231,19 @@ func (c *Client) Shutdown() {
}

type checkGateOptions struct {
logExposure bool
disableLogExposures bool
}

type getConfigOptions struct {
logExposure bool
disableLogExposures bool
}

type getExperimentOptions struct {
disableLogExposures bool
}

type getLayerOptions struct {
logExposure bool
disableLogExposures bool
}

type gateResponse struct {
Expand Down Expand Up @@ -271,16 +281,25 @@ func (c *Client) checkGateImpl(user User, gate string, options checkGateOptions)
serverRes := fetchGate(user, gate, c.transport)
res = &evalResult{Pass: serverRes.Value, Id: serverRes.RuleID}
} else {
if options.logExposure {
var exposure *ExposureEvent = nil
if !options.disableLogExposures {
context := &logContext{isManualExposure: false}
c.logger.logGateExposure(user, gate, res.Pass, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
exposure = c.logger.logGateExposure(user, gate, res.Pass, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
}
if c.options.EvaluationCallbacks.GateEvaluationCallback != nil {
c.options.EvaluationCallbacks.GateEvaluationCallback(gate, res.Pass, exposure)
}
}
return res.Pass
})
}

func (c *Client) getConfigImpl(user User, config string, options getConfigOptions) DynamicConfig {
type getConfigImplContext struct {
configOptions *getConfigOptions
experimentOptions *getExperimentOptions
}

func (c *Client) getConfigImpl(user User, config string, context getConfigImplContext) DynamicConfig {
return c.errorBoundary.captureGetConfig(func() DynamicConfig {
if !c.verifyUser(user) {
return *NewConfig(config, nil, "", "")
Expand All @@ -290,9 +309,22 @@ func (c *Client) getConfigImpl(user User, config string, options getConfigOption
if res.FetchFromServer {
res = c.fetchConfigFromServer(user, config)
} else {
if options.logExposure {
var exposure *ExposureEvent = nil
isExperiment := context.experimentOptions != nil
var logExposure bool
if isExperiment {
logExposure = !context.experimentOptions.disableLogExposures
} else {
logExposure = !context.configOptions.disableLogExposures
}
if logExposure {
context := &logContext{isManualExposure: false}
c.logger.logConfigExposure(user, config, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
exposure = c.logger.logConfigExposure(user, config, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
}
if isExperiment && c.options.EvaluationCallbacks.ExperimentEvaluationCallback != nil {
c.options.EvaluationCallbacks.ExperimentEvaluationCallback(config, res.ConfigValue, exposure)
} else if c.options.EvaluationCallbacks.ConfigEvaluationCallback != nil {
c.options.EvaluationCallbacks.ConfigEvaluationCallback(config, res.ConfigValue, exposure)
}
}
return res.ConfigValue
Expand All @@ -313,9 +345,13 @@ func (c *Client) getLayerImpl(user User, layer string, options getLayerOptions)
}

logFunc := func(config configBase, parameterName string) {
if options.logExposure {
var exposure *ExposureEvent = nil
if !options.disableLogExposures {
context := &logContext{isManualExposure: false}
c.logger.logLayerExposure(user, config, parameterName, *res, res.EvaluationDetails, context)
exposure = c.logger.logLayerExposure(user, config, parameterName, *res, res.EvaluationDetails, context)
}
if c.options.EvaluationCallbacks.LayerEvaluationCallback != nil {
c.options.EvaluationCallbacks.LayerEvaluationCallback(layer, parameterName, res.ConfigValue, exposure)
}
}

Expand Down
95 changes: 59 additions & 36 deletions evaluation_details_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package statsig

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
Expand All @@ -13,7 +12,10 @@ import (
const configSyncTime = 1631638014811

func TestEvaluationDetails(t *testing.T) {
events := []Event{}
gateExposures := make(map[string]ExposureEvent)
configExposures := make(map[string]ExposureEvent)
experimentExposures := make(map[string]ExposureEvent)
layerExposures := make(map[string]map[string]ExposureEvent)

getTestServer := func(dcsOnline bool) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
Expand All @@ -27,22 +29,28 @@ func TestEvaluationDetails(t *testing.T) {
res.WriteHeader(http.StatusOK)
_, _ = res.Write(bytes)
}
} else if strings.Contains(req.URL.Path, "log_event") {
type requestInput struct {
Events []Event `json:"events"`
StatsigMetadata statsigMetadata `json:"statsigMetadata"`
}
input := &requestInput{}
defer req.Body.Close()
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(req.Body)

_ = json.Unmarshal(buf.Bytes(), &input)
events = input.Events
}
}))
}

evaluationCallbacks := EvaluationCallbacks{
GateEvaluationCallback: func(name string, result bool, exposure *ExposureEvent) {
gateExposures[name] = *exposure
},
ConfigEvaluationCallback: func(name string, result DynamicConfig, exposure *ExposureEvent) {
configExposures[name] = *exposure
},
ExperimentEvaluationCallback: func(name string, result DynamicConfig, exposure *ExposureEvent) {
experimentExposures[name] = *exposure
},
LayerEvaluationCallback: func(name, param string, result DynamicConfig, exposure *ExposureEvent) {
if layerExposures[name] == nil {
layerExposures[name] = map[string]ExposureEvent{}
}
layerExposures[name][param] = *exposure
},
}

var opt *Options
var user User
reset := func() {
Expand All @@ -51,9 +59,13 @@ func TestEvaluationDetails(t *testing.T) {
Environment: Environment{Tier: "test"},
OutputLoggerOptions: getOutputLoggerOptionsForTest(t),
StatsigLoggerOptions: getStatsigLoggerOptionsForTest(t),
EvaluationCallbacks: evaluationCallbacks,
}
user = User{UserID: "some_user_id"}
events = []Event{}
gateExposures = make(map[string]ExposureEvent)
configExposures = make(map[string]ExposureEvent)
experimentExposures = make(map[string]ExposureEvent)
layerExposures = make(map[string]map[string]ExposureEvent)
}

start := func() {
Expand All @@ -80,32 +92,40 @@ func TestEvaluationDetails(t *testing.T) {
_ = CheckGate(user, "always_on_gate")
_ = GetConfig(user, "test_config")
_ = GetExperiment(user, "sample_experiment")
layer := GetLayer(user, "unallocated_layer")
layer.GetNumber("an_int", 0)
layer := GetLayer(user, "a_layer")
layer.GetBool("layer_param", false)
ShutdownAndDangerouslyClearInstance()

if len(events) != 3 {
t.Errorf("Should receive exactly 3 log_event. Got %d", len(events))
numEvents := len(gateExposures) + len(configExposures) + len(experimentExposures) + len(layerExposures)
if numEvents != 4 {
t.Errorf("Should receive exactly 4 log_event. Got %d", numEvents)
}

compareMetadata(t, events[0].Metadata, map[string]string{
compareMetadata(t, gateExposures["always_on_gate"].Metadata, map[string]string{
"gate": "always_on_gate",
"gateValue": "true",
"ruleID": "6N6Z8ODekNYZ7F8gFdoLP5",
"reason": "Network",
}, configSyncTime)

compareMetadata(t, events[1].Metadata, map[string]string{
compareMetadata(t, configExposures["test_config"].Metadata, map[string]string{
"config": "test_config",
"ruleID": "default",
"reason": "Network",
}, configSyncTime)

compareMetadata(t, events[2].Metadata, map[string]string{
compareMetadata(t, experimentExposures["sample_experiment"].Metadata, map[string]string{
"config": "sample_experiment",
"ruleID": "2RamGsERWbWMIMnSfOlQuX",
"reason": "Network",
}, configSyncTime)

compareMetadata(t, layerExposures["a_layer"]["layer_param"].Metadata, map[string]string{
"config": "a_layer",
"ruleID": "2RamGsERWbWMIMnSfOlQuX",
"parameterName": "layer_param",
"reason": "Network",
}, configSyncTime)
})

t.Run("bootstrap init reason", func(t *testing.T) {
Expand All @@ -117,24 +137,25 @@ func TestEvaluationDetails(t *testing.T) {
layer.GetNumber("an_int", 0)
ShutdownAndDangerouslyClearInstance()

if len(events) != 3 {
t.Errorf("Should receive exactly 3 log_event. Got %d", len(events))
numEvents := len(gateExposures) + len(configExposures) + len(experimentExposures)
if numEvents != 3 {
t.Errorf("Should receive exactly 3 log_event. Got %d", numEvents)
}

compareMetadata(t, events[0].Metadata, map[string]string{
compareMetadata(t, gateExposures["always_on_gate"].Metadata, map[string]string{
"gate": "always_on_gate",
"gateValue": "true",
"ruleID": "6N6Z8ODekNYZ7F8gFdoLP5",
"reason": "Bootstrap",
}, configSyncTime)

compareMetadata(t, events[1].Metadata, map[string]string{
compareMetadata(t, configExposures["test_config"].Metadata, map[string]string{
"config": "test_config",
"ruleID": "default",
"reason": "Bootstrap",
}, configSyncTime)

compareMetadata(t, events[2].Metadata, map[string]string{
compareMetadata(t, experimentExposures["sample_experiment"].Metadata, map[string]string{
"config": "sample_experiment",
"ruleID": "2RamGsERWbWMIMnSfOlQuX",
"reason": "Bootstrap",
Expand All @@ -150,24 +171,25 @@ func TestEvaluationDetails(t *testing.T) {
layer.GetNumber("an_int", 0)
ShutdownAndDangerouslyClearInstance()

if len(events) != 3 {
t.Errorf("Should receive exactly 3 log_event. Got %d", len(events))
numEvents := len(gateExposures) + len(configExposures) + len(experimentExposures)
if numEvents != 3 {
t.Errorf("Should receive exactly 3 log_event. Got %d", numEvents)
}

compareMetadata(t, events[0].Metadata, map[string]string{
compareMetadata(t, gateExposures["always_on_gate"].Metadata, map[string]string{
"gate": "always_on_gate",
"gateValue": "false",
"ruleID": "",
"reason": "Unrecognized",
}, 0)

compareMetadata(t, events[1].Metadata, map[string]string{
compareMetadata(t, configExposures["test_config"].Metadata, map[string]string{
"config": "test_config",
"ruleID": "",
"reason": "Unrecognized",
}, 0)

compareMetadata(t, events[2].Metadata, map[string]string{
compareMetadata(t, experimentExposures["sample_experiment"].Metadata, map[string]string{
"config": "sample_experiment",
"ruleID": "",
"reason": "Unrecognized",
Expand All @@ -182,18 +204,19 @@ func TestEvaluationDetails(t *testing.T) {
_ = GetConfig(user, "test_config")
ShutdownAndDangerouslyClearInstance()

if len(events) != 2 {
t.Errorf("Should receive exactly 2 log_event. Got %d", len(events))
numEvents := len(gateExposures) + len(configExposures) + len(experimentExposures)
if numEvents != 2 {
t.Errorf("Should receive exactly 2 log_event. Got %d", numEvents)
}

compareMetadata(t, events[0].Metadata, map[string]string{
compareMetadata(t, gateExposures["always_on_gate"].Metadata, map[string]string{
"gate": "always_on_gate",
"gateValue": "false",
"ruleID": "override",
"reason": "LocalOverride",
}, configSyncTime)

compareMetadata(t, events[1].Metadata, map[string]string{
compareMetadata(t, configExposures["test_config"].Metadata, map[string]string{
"config": "test_config",
"ruleID": "override",
"reason": "LocalOverride",
Expand Down
Loading

0 comments on commit 6559260

Please sign in to comment.