Skip to content

Commit

Permalink
add hash algo + hash secondary and undelegated exposures (#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
kat-statsig authored Aug 2, 2024
1 parent 6c9e4ad commit d61c079
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 96 deletions.
61 changes: 39 additions & 22 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,29 @@ func (c *Client) initInBackground() {
// 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).Value
context := StatsigContext{Caller: "checkGate", ConfigName: gate}
return c.checkGateImpl(user, gate, options, context).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
context := StatsigContext{Caller: "checkGateWithExposureLoggingDisabled", ConfigName: gate}
return c.checkGateImpl(user, gate, options, context).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)
context := StatsigContext{Caller: "getGate", ConfigName: gate}
return c.checkGateImpl(user, gate, options, context)
}

// 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)
context := StatsigContext{Caller: "getGateWithExposureLoggingDisabled", ConfigName: gate}
return c.checkGateImpl(user, gate, options, context)
}

// Logs an exposure event for the dynamic config
Expand All @@ -113,7 +117,7 @@ func (c *Client) ManuallyLogGateExposure(user User, gate string) {
return
}
user = normalizeUser(user, *c.options)
res := c.evaluator.evalGate(user, gate)
res := c.evaluator.evalGate(user, gate, StatsigContext{Caller: "logGateExposure", ConfigName: gate})
context := &logContext{isManualExposure: true}
c.logger.logGateExposure(user, gate, res.Value, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
})
Expand All @@ -123,14 +127,16 @@ func (c *Client) ManuallyLogGateExposure(user User, gate string) {
func (c *Client) GetConfig(user User, config string) DynamicConfig {
options := &getConfigOptions{disableLogExposures: false}
context := getConfigImplContext{configOptions: options}
return c.getConfigImpl(user, config, context)
statsigContext := StatsigContext{Caller: "getConfig", ConfigName: config}
return c.getConfigImpl(user, config, context, statsigContext)
}

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

// Logs an exposure event for the config
Expand All @@ -140,7 +146,7 @@ func (c *Client) ManuallyLogConfigExposure(user User, config string) {
return
}
user = normalizeUser(user, *c.options)
res := c.evaluator.evalConfig(user, config, nil)
res := c.evaluator.evalConfig(user, config, nil, StatsigContext{Caller: "logConfigExposure", ConfigName: config})
context := &logContext{isManualExposure: true}
c.logger.logConfigExposure(user, config, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
})
Expand All @@ -160,7 +166,8 @@ func (c *Client) GetExperiment(user User, experiment string) DynamicConfig {
}
options := &GetExperimentOptions{DisableLogExposures: false}
context := getConfigImplContext{experimentOptions: options}
return c.getConfigImpl(user, experiment, context)
statsigContext := StatsigContext{Caller: "getExperiment", ConfigName: experiment}
return c.getConfigImpl(user, experiment, context, statsigContext)
}

// Gets the DynamicConfig value of an Experiment for the given user without logging an exposure event
Expand All @@ -170,7 +177,8 @@ func (c *Client) GetExperimentWithExposureLoggingDisabled(user User, experiment
}
options := &GetExperimentOptions{DisableLogExposures: true}
context := getConfigImplContext{experimentOptions: options}
return c.getConfigImpl(user, experiment, context)
statsigContext := StatsigContext{Caller: "getExperimentWithExposureLoggingDisabled", ConfigName: experiment}
return c.getConfigImpl(user, experiment, context, statsigContext)
}

// Gets the DynamicConfig value of an Experiment for the given user with configurable options
Expand All @@ -179,7 +187,8 @@ func (c *Client) GetExperimentWithOptions(user User, experiment string, options
return *NewConfig(experiment, nil, "", "", nil)
}
context := getConfigImplContext{experimentOptions: options}
return c.getConfigImpl(user, experiment, context)
statsigContext := StatsigContext{Caller: "getExperimentWithOptions", ConfigName: experiment}
return c.getConfigImpl(user, experiment, context, statsigContext)
}

// Logs an exposure event for the experiment
Expand All @@ -201,18 +210,21 @@ func (c *Client) GetUserPersistedValues(user User, idType string) UserPersistedV
// Gets the Layer object for the given user
func (c *Client) GetLayer(user User, layer string) Layer {
options := &GetLayerOptions{DisableLogExposures: false, PersistedValues: nil}
return c.getLayerImpl(user, layer, options)
context := StatsigContext{Caller: "getLayer", ConfigName: layer}
return c.getLayerImpl(user, layer, options, context)
}

// Gets the Layer object for the given user without logging an exposure event
func (c *Client) GetLayerWithExposureLoggingDisabled(user User, layer string) Layer {
options := &GetLayerOptions{DisableLogExposures: true, PersistedValues: nil}
return c.getLayerImpl(user, layer, options)
context := StatsigContext{Caller: "getLayerWithExposureLoggingDisabled", ConfigName: layer}
return c.getLayerImpl(user, layer, options, context)
}

// Gets the Layer object for the given user with configurable options
func (c *Client) GetLayerWithOptions(user User, layer string, options *GetLayerOptions) Layer {
return c.getLayerImpl(user, layer, options)
context := StatsigContext{Caller: "getLayerWithOptions", ConfigName: layer}
return c.getLayerImpl(user, layer, options, context)
}

// Logs an exposure event for the parameter in the given layer
Expand All @@ -222,7 +234,7 @@ func (c *Client) ManuallyLogLayerParameterExposure(user User, layer string, para
return
}
user = normalizeUser(user, *c.options)
res := c.evaluator.evalLayer(user, layer, nil)
res := c.evaluator.evalLayer(user, layer, nil, StatsigContext{Caller: "logLayerParameterExposure", ConfigName: layer})
config := NewLayer(layer, res.JsonValue, res.RuleID, res.GroupName, nil, res.ConfigDelegate)
context := &logContext{isManualExposure: true}
c.logger.logLayerExposure(user, *config, parameter, *res, res.EvaluationDetails, context)
Expand Down Expand Up @@ -272,6 +284,7 @@ func (c *Client) GetClientInitializeResponse(user User, clientKey string, includ
options := &GCIROptions{
IncludeLocalOverrides: includeLocalOverrides,
ClientKey: clientKey,
HashAlgorithm: "sha256",
}
return c.GetClientInitializeResponseImpl(user, options)
}
Expand All @@ -288,7 +301,11 @@ func (c *Client) GetClientInitializeResponseImpl(user User, options *GCIROptions
user = normalizeUser(user, *c.options)
includeLocalOverrides := options.IncludeLocalOverrides
clientKey := options.ClientKey
response := c.evaluator.getClientInitializeResponse(user, clientKey, includeLocalOverrides)
hashAlgorithm := options.HashAlgorithm
if hashAlgorithm != "none" && hashAlgorithm != "djb2" {
hashAlgorithm = "sha256"
}
response := c.evaluator.getClientInitializeResponse(user, clientKey, includeLocalOverrides, hashAlgorithm)
if response.Time == 0 {
c.errorBoundary.logException(errors.New("empty response from server"))
}
Expand Down Expand Up @@ -356,13 +373,13 @@ type getConfigInput struct {
StatsigMetadata statsigMetadata `json:"statsigMetadata"`
}

func (c *Client) checkGateImpl(user User, name string, options checkGateOptions) FeatureGate {
func (c *Client) checkGateImpl(user User, name string, options checkGateOptions, context StatsigContext) FeatureGate {
return c.errorBoundary.captureCheckGate(func() FeatureGate {
if !c.verifyUser(user) {
return *NewGate(name, false, "", "", nil)
}
user = normalizeUser(user, *c.options)
res := c.evaluator.evalGate(user, name)
res := c.evaluator.evalGate(user, name, context)
if res.FetchFromServer {
serverRes := fetchGate(user, name, c.transport)
res = &evalResult{Value: serverRes.Value, RuleID: serverRes.RuleID}
Expand Down Expand Up @@ -398,7 +415,7 @@ type getConfigImplContext struct {
experimentOptions *GetExperimentOptions
}

func (c *Client) getConfigImpl(user User, name string, context getConfigImplContext) DynamicConfig {
func (c *Client) getConfigImpl(user User, name string, context getConfigImplContext, statsigContext StatsigContext) DynamicConfig {
return c.errorBoundary.captureGetConfig(func() DynamicConfig {
if !c.verifyUser(user) {
return *NewConfig(name, nil, "", "", nil)
Expand All @@ -409,7 +426,7 @@ func (c *Client) getConfigImpl(user User, name string, context getConfigImplCont
persistedValues = context.experimentOptions.PersistedValues
}
user = normalizeUser(user, *c.options)
res := c.evaluator.evalConfig(user, name, persistedValues)
res := c.evaluator.evalConfig(user, name, persistedValues, statsigContext)
config := *NewConfig(name, res.JsonValue, res.RuleID, res.GroupName, res.EvaluationDetails)
if res.FetchFromServer {
res = c.fetchConfigFromServer(user, name)
Expand Down Expand Up @@ -453,14 +470,14 @@ func (c *Client) getConfigImpl(user User, name string, context getConfigImplCont
})
}

func (c *Client) getLayerImpl(user User, name string, options *GetLayerOptions) Layer {
func (c *Client) getLayerImpl(user User, name string, options *GetLayerOptions, context StatsigContext) Layer {
return c.errorBoundary.captureGetLayer(func() Layer {
if !c.verifyUser(user) {
return *NewLayer(name, nil, "", "", nil, "")
}

user = normalizeUser(user, *c.options)
res := c.evaluator.evalLayer(user, name, options.PersistedValues)
res := c.evaluator.evalLayer(user, name, options.PersistedValues, context)

if res.FetchFromServer {
res = c.fetchConfigFromServer(user, name)
Expand Down
19 changes: 11 additions & 8 deletions client_initialize_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@ func getClientInitializeResponse(
e *evaluator,
clientKey string,
includeLocalOverrides bool,
hashAlgorithm string,
) ClientInitializeResponse {
context := StatsigContext{Caller: "getClientInitializeResponse", Hash: hashAlgorithm}

evalResultToBaseResponse := func(name string, eval *evalResult) (string, baseSpecInitializeResponse) {
hashedName := getHashBase64StringEncoding(name)
hashedName := hashName(hashAlgorithm, name)
result := baseSpecInitializeResponse{
Name: hashedName,
RuleID: eval.RuleID,
Expand All @@ -83,10 +86,10 @@ func getClientInitializeResponse(
if gateOverride, hasOverride := e.getGateOverrideEval(gateName); hasOverride {
evalRes = gateOverride
} else {
evalRes = e.eval(user, spec, 0)
evalRes = e.eval(user, spec, 0, context)
}
} else {
evalRes = e.eval(user, spec, 0)
evalRes = e.eval(user, spec, 0, context)
}
hashedName, base := evalResultToBaseResponse(gateName, evalRes)
result := GateInitializeResponse{
Expand All @@ -101,10 +104,10 @@ func getClientInitializeResponse(
if configOverride, hasOverride := e.getConfigOverrideEval(configName); hasOverride {
evalRes = configOverride
} else {
evalRes = e.eval(user, spec, 0)
evalRes = e.eval(user, spec, 0, context)
}
} else {
evalRes = e.eval(user, spec, 0)
evalRes = e.eval(user, spec, 0, context)
}
hashedName, base := evalResultToBaseResponse(configName, evalRes)
result := ConfigInitializeResponse{
Expand Down Expand Up @@ -137,7 +140,7 @@ func getClientInitializeResponse(
return hashedName, result
}
layerToResponse := func(layerName string, spec configSpec) (string, LayerInitializeResponse) {
evalResult := e.eval(user, spec, 0)
evalResult := e.eval(user, spec, 0, StatsigContext{Hash: hashAlgorithm})
hashedName, base := evalResultToBaseResponse(layerName, evalResult)
result := LayerInitializeResponse{
baseSpecInitializeResponse: base,
Expand All @@ -157,9 +160,9 @@ func getClientInitializeResponse(
}
if delegate != "" {
delegateSpec, exists := e.store.getDynamicConfig(delegate)
delegateResult := e.eval(user, delegateSpec, 0)
delegateResult := e.eval(user, delegateSpec, 0, context)
if exists {
result.AllocatedExperimentName = getHashBase64StringEncoding(delegate)
result.AllocatedExperimentName = hashName(hashAlgorithm, delegate)
result.IsUserInExperiment = new(bool)
*result.IsUserInExperiment = delegateResult.IsExperimentGroup != nil && *delegateResult.IsExperimentGroup
result.IsExperimentActive = new(bool)
Expand Down
19 changes: 6 additions & 13 deletions error_boundary.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,6 @@ type logExceptionRequestBody struct {
Tag string `json:"tag"`
}

type logExceptionOptions struct {
Tag string
Extra map[string]interface{}
BypassDedupe bool
LogToOutput bool
}

type logExceptionResponse struct {
Success bool
}
Expand Down Expand Up @@ -134,7 +127,7 @@ func (e *errorBoundary) ebRecover(recoverCallback func()) {
}
}

func (e *errorBoundary) logExceptionWithOptions(exception error, options logExceptionOptions) {
func (e *errorBoundary) logExceptionWithContext(exception error, context StatsigContext) {
if e.options.StatsigLoggerOptions.DisableAllLogging || e.options.LocalMode {
return
}
Expand All @@ -145,10 +138,10 @@ func (e *errorBoundary) logExceptionWithOptions(exception error, options logExce
exceptionString = exception.Error()
}

if options.LogToOutput {
if context.LogToOutput {
Logger().LogError(exception)
}
if !options.BypassDedupe && e.checkSeen(exceptionString) {
if !context.BypassDedupe && e.checkSeen(exceptionString) {
return
}
stack := make([]byte, 1024)
Expand All @@ -158,8 +151,8 @@ func (e *errorBoundary) logExceptionWithOptions(exception error, options logExce
Exception: exceptionString,
Info: string(stack),
StatsigMetadata: metadata,
Extra: options.Extra,
Tag: options.Tag,
Extra: context.getContextForLogging(),
Tag: context.Caller,
}
bodyString, err := json.Marshal(body)
if err != nil {
Expand All @@ -181,5 +174,5 @@ func (e *errorBoundary) logExceptionWithOptions(exception error, options logExce
}

func (e *errorBoundary) logException(exception error) {
e.logExceptionWithOptions(exception, logExceptionOptions{})
e.logExceptionWithContext(exception, StatsigContext{})
}
6 changes: 3 additions & 3 deletions evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func test_helper(apiOverride string, t *testing.T) {
for _, entry := range d.Entries {
u := entry.User
for gate, serverResult := range entry.GatesV2 {
sdkResult := c.evaluator.evalGate(u, gate)
sdkResult := c.evaluator.evalGate(u, gate, StatsigContext{Hash: "none"})
if sdkResult.Value != serverResult.Value {
t.Errorf("Values are different for gate %s. SDK got %t but server is %t. User is %+v",
gate, sdkResult.Value, serverResult.Value, u)
Expand All @@ -147,7 +147,7 @@ func test_helper(apiOverride string, t *testing.T) {
}

for config, serverResult := range entry.Configs {
sdkResult := c.evaluator.evalConfig(u, config, nil)
sdkResult := c.evaluator.evalConfig(u, config, nil, StatsigContext{Hash: "none"})
if !reflect.DeepEqual(sdkResult.JsonValue, serverResult.Value) {
t.Errorf("Values are different for config %s. SDK got %s but server is %s. User is %+v",
config, sdkResult.JsonValue, serverResult.Value, u)
Expand All @@ -171,7 +171,7 @@ func test_helper(apiOverride string, t *testing.T) {
}

for layer, serverResult := range entry.Layers {
sdkResult := c.evaluator.evalLayer(u, layer, nil)
sdkResult := c.evaluator.evalLayer(u, layer, nil, StatsigContext{Hash: "none"})
if !reflect.DeepEqual(sdkResult.JsonValue, serverResult.Value) {
t.Errorf("Values are different for layer %s. SDK got %s but server is %s. User is %+v",
layer, sdkResult.JsonValue, serverResult.Value, u)
Expand Down
Loading

0 comments on commit d61c079

Please sign in to comment.