Skip to content

Commit

Permalink
GetGate API
Browse files Browse the repository at this point in the history
  • Loading branch information
kenny-statsig committed Dec 1, 2023
1 parent 00bf8ed commit e1444dc
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 32 deletions.
34 changes: 23 additions & 11 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,23 @@ 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{disableLogExposures: false}
return c.checkGateImpl(user, gate, options)
return c.checkGateImpl(user, gate, options).Value
}

// 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{disableLogExposures: true}
return c.checkGateImpl(user, gate, options).Value
}

// Get the Feature Gate for the given user
func (c *Client) GetGate(user User, gate string) FeatureGate {
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) GetGateWithExposureLoggingDisabled(user User, gate string) FeatureGate {
options := checkGateOptions{disableLogExposures: true}
return c.checkGateImpl(user, gate, options)
}
Expand All @@ -71,7 +83,7 @@ func (c *Client) ManuallyLogGateExposure(user User, gate string) {
user = normalizeUser(user, *c.options)
res := c.evaluator.checkGate(user, gate)
context := &logContext{isManualExposure: true}
c.logger.logGateExposure(user, gate, res.Pass, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
c.logger.logGateExposure(user, gate, res.Pass, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
})
}

Expand All @@ -98,7 +110,7 @@ func (c *Client) ManuallyLogConfigExposure(user User, config string) {
user = normalizeUser(user, *c.options)
res := c.evaluator.getConfig(user, config)
context := &logContext{isManualExposure: true}
c.logger.logConfigExposure(user, config, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
c.logger.logConfigExposure(user, config, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
})
}

Expand Down Expand Up @@ -264,27 +276,27 @@ type getConfigInput struct {
StatsigMetadata statsigMetadata `json:"statsigMetadata"`
}

func (c *Client) checkGateImpl(user User, gate string, options checkGateOptions) bool {
return c.errorBoundary.captureCheckGate(func() bool {
func (c *Client) checkGateImpl(user User, gate string, options checkGateOptions) FeatureGate {
return c.errorBoundary.captureCheckGate(func() FeatureGate {
if !c.verifyUser(user) {
return false
return *NewGate(gate, false, "", "")
}
user = normalizeUser(user, *c.options)
res := c.evaluator.checkGate(user, gate)
if res.FetchFromServer {
serverRes := fetchGate(user, gate, c.transport)
res = &evalResult{Pass: serverRes.Value, Id: serverRes.RuleID}
res = &evalResult{Pass: serverRes.Value, RuleID: serverRes.RuleID}
} else {
var exposure *ExposureEvent = nil
if !options.disableLogExposures {
context := &logContext{isManualExposure: false}
exposure = c.logger.logGateExposure(user, gate, res.Pass, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
exposure = c.logger.logGateExposure(user, gate, res.Pass, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
}
if c.options.EvaluationCallbacks.GateEvaluationCallback != nil {
c.options.EvaluationCallbacks.GateEvaluationCallback(gate, res.Pass, exposure)
}
}
return res.Pass
return *NewGate(gate, res.Pass, res.RuleID, res.GroupName)
})
}

Expand Down Expand Up @@ -313,7 +325,7 @@ func (c *Client) getConfigImpl(user User, config string, context getConfigImplCo
}
if logExposure {
context := &logContext{isManualExposure: false}
exposure = c.logger.logConfigExposure(user, config, res.Id, res.SecondaryExposures, res.EvaluationDetails, context)
exposure = c.logger.logConfigExposure(user, config, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
}
if isExperiment && c.options.EvaluationCallbacks.ExperimentEvaluationCallback != nil {
c.options.EvaluationCallbacks.ExperimentEvaluationCallback(config, res.ConfigValue, exposure)
Expand Down Expand Up @@ -408,6 +420,6 @@ func (c *Client) fetchConfigFromServer(user User, configName string) *evalResult
serverRes := fetchConfig(user, configName, c.transport)
return &evalResult{
ConfigValue: *NewConfig(configName, serverRes.Value, serverRes.RuleID, ""),
Id: serverRes.RuleID,
RuleID: serverRes.RuleID,
}
}
6 changes: 3 additions & 3 deletions client_initialize_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func getClientInitializeResponse(
hashedName := getHashBase64StringEncoding(name)
result := baseSpecInitializeResponse{
Name: hashedName,
RuleID: eval.Id,
RuleID: eval.RuleID,
SecondaryExposures: cleanExposures(eval.SecondaryExposures),
}
return hashedName, result
Expand All @@ -100,7 +100,7 @@ func getClientInitializeResponse(
result := ConfigInitializeResponse{
baseSpecInitializeResponse: base,
Value: evalResult.ConfigValue.Value,
Group: evalResult.Id,
Group: evalResult.RuleID,
IsDeviceBased: strings.ToLower(spec.IDType) == "stableid",
}
entityType := strings.ToLower(spec.Entity)
Expand Down Expand Up @@ -135,7 +135,7 @@ func getClientInitializeResponse(
result := LayerInitializeResponse{
baseSpecInitializeResponse: base,
Value: evalResult.ConfigValue.Value,
Group: evalResult.Id,
Group: evalResult.RuleID,
IsDeviceBased: strings.ToLower(spec.IDType) == "stableid",
UndelegatedSecondaryExposures: cleanExposures(evalResult.UndelegatedSecondaryExposures),
}
Expand Down
2 changes: 1 addition & 1 deletion error_boundary.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (e *errorBoundary) checkSeen(exceptionString string) bool {
return false
}

func (e *errorBoundary) captureCheckGate(task func() bool) bool {
func (e *errorBoundary) captureCheckGate(task func() FeatureGate) FeatureGate {
defer e.ebRecover(func() {
e.diagnostics.api().checkGate().end().success(false).mark()
})
Expand Down
12 changes: 6 additions & 6 deletions evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ func test_helper(apiOverride string, t *testing.T) {
gate, sdkResult.Pass, serverResult.Value, u)
}

if sdkResult.Id != serverResult.RuleID {
if sdkResult.RuleID != serverResult.RuleID {
t.Errorf("Rule IDs are different for gate %s. SDK got %s but server is %s. User is %+v",
gate, sdkResult.Id, serverResult.RuleID, u)
gate, sdkResult.RuleID, serverResult.RuleID, u)
}

if !compare_secondary_exp(t, sdkResult.SecondaryExposures, serverResult.SecondaryExposures) {
Expand All @@ -151,9 +151,9 @@ func test_helper(apiOverride string, t *testing.T) {
config, sdkResult.ConfigValue.Value, serverResult.Value, u)
}

if sdkResult.Id != serverResult.RuleID {
if sdkResult.RuleID != serverResult.RuleID {
t.Errorf("Rule IDs are different for config %s. SDK got %s but server is %s",
config, sdkResult.Id, serverResult.RuleID)
config, sdkResult.RuleID, serverResult.RuleID)
}

if sdkResult.ConfigValue.GroupName != serverResult.GroupName {
Expand All @@ -175,9 +175,9 @@ func test_helper(apiOverride string, t *testing.T) {
layer, sdkResult.ConfigValue.Value, serverResult.Value, u)
}

if sdkResult.Id != serverResult.RuleID {
if sdkResult.RuleID != serverResult.RuleID {
t.Errorf("Rule IDs are different for layer %s. SDK got %s but server is %s",
layer, sdkResult.Id, serverResult.RuleID)
layer, sdkResult.RuleID, serverResult.RuleID)
}

if sdkResult.ConfigValue.GroupName != serverResult.GroupName {
Expand Down
21 changes: 12 additions & 9 deletions evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ type evalResult struct {
Pass bool
ConfigValue DynamicConfig
FetchFromServer bool
Id string
RuleID string
GroupName string
SecondaryExposures []map[string]string
UndelegatedSecondaryExposures []map[string]string
ConfigDelegate string
Expand Down Expand Up @@ -91,7 +92,7 @@ func (e *evaluator) evalGate(user User, gateName string, depth int) *evalResult
evalDetails := e.createEvaluationDetails(reasonLocalOverride)
return &evalResult{
Pass: gateOverride,
Id: "override",
RuleID: "override",
EvaluationDetails: evalDetails,
SecondaryExposures: make([]map[string]string, 0),
}
Expand All @@ -115,7 +116,7 @@ func (e *evaluator) evalConfig(user User, configName string, depth int) *evalRes
return &evalResult{
Pass: true,
ConfigValue: *NewConfig(configName, configOverride, "override", ""),
Id: "override",
RuleID: "override",
EvaluationDetails: evalDetails,
SecondaryExposures: make([]map[string]string, 0),
}
Expand All @@ -139,7 +140,7 @@ func (e *evaluator) evalLayer(user User, name string, depth int) *evalResult {
return &evalResult{
Pass: true,
ConfigValue: *NewConfig(name, layerOverride, "override", ""),
Id: "override",
RuleID: "override",
EvaluationDetails: evalDetails,
SecondaryExposures: make([]map[string]string, 0),
}
Expand Down Expand Up @@ -247,7 +248,8 @@ func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult {
result := &evalResult{
Pass: pass,
ConfigValue: *NewConfig(spec.Name, configValue, rule.ID, rule.GroupName),
Id: rule.ID,
RuleID: rule.ID,
GroupName: rule.GroupName,
SecondaryExposures: exposures,
UndelegatedSecondaryExposures: exposures,
EvaluationDetails: evalDetails,
Expand All @@ -259,7 +261,8 @@ func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult {
} else {
return &evalResult{
Pass: pass,
Id: rule.ID,
RuleID: rule.ID,
GroupName: rule.GroupName,
SecondaryExposures: exposures,
EvaluationDetails: evalDetails,
}
Expand All @@ -274,13 +277,13 @@ func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult {
return &evalResult{
Pass: false,
ConfigValue: *NewConfig(spec.Name, configValue, defaultRuleID, ""),
Id: defaultRuleID,
RuleID: defaultRuleID,
SecondaryExposures: exposures,
UndelegatedSecondaryExposures: exposures,
EvaluationDetails: evalDetails,
}
}
return &evalResult{Pass: false, Id: defaultRuleID, SecondaryExposures: exposures}
return &evalResult{Pass: false, RuleID: defaultRuleID, SecondaryExposures: exposures}
}

func (e *evaluator) evalDelegate(user User, rule configRule, exposures []map[string]string, depth int) *evalResult {
Expand Down Expand Up @@ -361,7 +364,7 @@ func (e *evaluator) evalCondition(user User, cond configCondition, depth int) *e
newExposure := map[string]string{
"gate": dependentGateName,
"gateValue": strconv.FormatBool(result.Pass),
"ruleID": result.Id,
"ruleID": result.RuleID,
}
allExposures := append(result.SecondaryExposures, newExposure)
if condType == "pass_gate" {
Expand Down
36 changes: 34 additions & 2 deletions manual_exposure_test.go → exposure_logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"testing"
)

func TestManualExposure(t *testing.T) {
func TestExposureLogging(t *testing.T) {
events := []Event{}

testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
Expand Down Expand Up @@ -40,18 +40,50 @@ func TestManualExposure(t *testing.T) {
StatsigLoggerOptions: getStatsigLoggerOptionsForTest(t),
}

user := User{UserID: "some_user_id"}
user := User{UserID: "some_user_id", Email: "[email protected]"}

start := func() {
events = []Event{}
InitializeWithOptions("secret-key", opt)
}

t.Run("logs exposures for all API", func(t *testing.T) {
start()
gateValue := CheckGate(user, "always_on_gate")
gate := GetGate(user, "always_on_gate")
config := GetConfig(user, "test_config")
experiment := GetExperiment(user, "sample_experiment")
layer := GetLayer(user, "a_layer")
layer.GetString("experiment_param", "")
ShutdownAndDangerouslyClearInstance()

if len(events) != 5 {
t.Errorf("Should receive exactly 5 log_events")
}

if gateValue != gate.Value {
t.Errorf("CheckGate and GetGate returned different results: %+v vs %+v", gateValue, gate.Value)
}
if gate.GroupName != "everyone" {
t.Errorf("Gate expected group name %+v but received %+v", "everyone", gate.GroupName)
}
if config.GroupName != "statsig email" {
t.Errorf("Config expected group name %+v but received %+v", "statsig email", config.GroupName)
}
if experiment.GroupName != "Control" {
t.Errorf("Experiment expected group name %+v but received %+v", "Control", experiment.GroupName)
}
if layer.GroupName != "Control" {
t.Errorf("Layer expected group name %+v but received %+v", "Control", layer.GroupName)
}
})

//

t.Run("does not log for exposure logging disabled API", func(t *testing.T) {
start()
CheckGateWithExposureLoggingDisabled(user, "always_on_gate")
GetGateWithExposureLoggingDisabled(user, "always_on_gate")
GetConfigWithExposureLoggingDisabled(user, "test_config")
GetExperimentWithExposureLoggingDisabled(user, "sample_experiment")
layer := GetLayerWithExposureLoggingDisabled(user, "a_layer")
Expand Down
16 changes: 16 additions & 0 deletions statsig.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ func CheckGateWithExposureLoggingDisabled(user User, gate string) bool {
return instance.CheckGateWithExposureLoggingDisabled(user, gate)
}

// Get the Feature Gate for the given user
func GetGate(user User, gate string) FeatureGate {
if !IsInitialized() {
panic(fmt.Errorf("must Initialize() statsig before calling GetGate"))
}
return instance.GetGate(user, gate)
}

// Get the Feature Gate for the given user without logging an exposure event
func GetGateWithExposureLoggingDisabled(user User, gate string) FeatureGate {
if !IsInitialized() {
panic(fmt.Errorf("must Initialize() statsig before calling GetGateWithExposureLoggingDisabled"))
}
return instance.GetGateWithExposureLoggingDisabled(user, gate)
}

// Logs an exposure event for the gate
func ManuallyLogGateExposure(user User, config string) {
if !IsInitialized() {
Expand Down
17 changes: 17 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ type configBase struct {
LogExposure *func(configBase, string)
}

type FeatureGate struct {
Name string `json:"name"`
Value bool `json:"value"`
RuleID string `json:"rule_id"`
GroupName string `json:"group_name"`
LogExposure *func(configBase, string)
}

// A json blob configured in the Statsig Console
type DynamicConfig struct {
configBase
Expand All @@ -45,6 +53,15 @@ type Layer struct {
configBase
}

func NewGate(name string, value bool, ruleID string, groupName string) *FeatureGate {
return &FeatureGate{
Name: name,
Value: value,
RuleID: ruleID,
GroupName: groupName,
}
}

func NewConfig(name string, value map[string]interface{}, ruleID string, groupName string) *DynamicConfig {
if value == nil {
value = make(map[string]interface{})
Expand Down

0 comments on commit e1444dc

Please sign in to comment.