Skip to content

Commit

Permalink
filter & dedupe exposures during evaluation (#184)
Browse files Browse the repository at this point in the history
  • Loading branch information
kenny-statsig authored May 22, 2024
1 parent 45ad920 commit 8285fec
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 23 deletions.
18 changes: 2 additions & 16 deletions client_initialize_response.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package statsig

import (
"fmt"
"strings"
)

Expand Down Expand Up @@ -57,19 +56,6 @@ type LayerInitializeResponse struct {
UndelegatedSecondaryExposures []map[string]string `json:"undelegated_secondary_exposures"`
}

func cleanExposures(exposures []map[string]string) []map[string]string {
seen := make(map[string]bool)
result := make([]map[string]string, 0)
for _, exposure := range exposures {
key := fmt.Sprintf("%s|%s|%s", exposure["gate"], exposure["gateValue"], exposure["ruleID"])
if _, exists := seen[key]; !exists {
seen[key] = true
result = append(result, exposure)
}
}
return result
}

func mergeMaps(a map[string]interface{}, b map[string]interface{}) {
for k, v := range b {
a[k] = v
Expand All @@ -87,7 +73,7 @@ func getClientInitializeResponse(
result := baseSpecInitializeResponse{
Name: hashedName,
RuleID: eval.RuleID,
SecondaryExposures: cleanExposures(eval.SecondaryExposures),
SecondaryExposures: eval.SecondaryExposures,
}
return hashedName, result
}
Expand Down Expand Up @@ -158,7 +144,7 @@ func getClientInitializeResponse(
Value: evalResult.JsonValue,
Group: evalResult.RuleID,
IsDeviceBased: strings.ToLower(spec.IDType) == "stableid",
UndelegatedSecondaryExposures: cleanExposures(evalResult.UndelegatedSecondaryExposures),
UndelegatedSecondaryExposures: evalResult.UndelegatedSecondaryExposures,
}
delegate := evalResult.ConfigDelegate
result.ExplicitParameters = new([]string)
Expand Down
30 changes: 23 additions & 7 deletions evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (e *evaluator) evalGateImpl(user User, gateName string, depth int) *evalRes
}
}
if gate, hasGate := e.store.getGate(gateName); hasGate {
return e.eval(user, gate, depth+1)
return e.eval(user, gate, depth)
}
emptyEvalResult := new(evalResult)
emptyEvalResult.EvaluationDetails = e.createEvaluationDetails(reasonUnrecognized)
Expand Down Expand Up @@ -169,7 +169,7 @@ func (e *evaluator) evalConfigImpl(user User, configName string, persistedValues
}

if persistedValues == nil || config.IsActive == nil || !*config.IsActive {
return e.evalAndSDeleteFromPersistentStorage(user, config, depth)
return e.evalAndDeleteFromPersistentStorage(user, config, depth)
}

stickyResult := newEvalResultFromUserPersistedValues(configName, persistedValues)
Expand Down Expand Up @@ -204,15 +204,15 @@ func (e *evaluator) evalLayerImpl(user User, name string, persistedValues UserPe
}

if persistedValues == nil {
return e.evalAndSDeleteFromPersistentStorage(user, config, depth)
return e.evalAndDeleteFromPersistentStorage(user, config, depth)
}

stickyResult := newEvalResultFromUserPersistedValues(name, persistedValues)
if stickyResult != nil {
if e.allocatedExperimentExistsAndIsActive(stickyResult) {
return stickyResult
} else {
return e.evalAndSDeleteFromPersistentStorage(user, config, depth)
return e.evalAndDeleteFromPersistentStorage(user, config, depth)
}
} else {
evaluation := e.eval(user, config, depth)
Expand Down Expand Up @@ -240,7 +240,7 @@ func (e *evaluator) evalAndSaveToPersistentStorage(user User, config configSpec,
return evaluation
}

func (e *evaluator) evalAndSDeleteFromPersistentStorage(user User, config configSpec, depth int) *evalResult {
func (e *evaluator) evalAndDeleteFromPersistentStorage(user User, config configSpec, depth int) *evalResult {
e.persistentStorageUtils.delete(user, config.IDType, config.Name)
return e.eval(user, config, depth)
}
Expand Down Expand Up @@ -323,6 +323,22 @@ func (e *evaluator) getClientInitializeResponse(user User, clientKey string, inc
return getClientInitializeResponse(user, e, clientKey, includeLocalOverrides)
}

func (e *evaluator) cleanExposures(exposures []map[string]string) []map[string]string {
seen := make(map[string]bool)
result := make([]map[string]string, 0)
for _, exposure := range exposures {
if strings.HasPrefix(exposure["gate"], "segment:") {
continue
}
key := fmt.Sprintf("%s|%s|%s", exposure["gate"], exposure["gateValue"], exposure["ruleID"])
if _, exists := seen[key]; !exists {
seen[key] = true
result = append(result, exposure)
}
}
return result
}

func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult {
if depth > maxRecursiveDepth {
panic(errors.New("Statsig Evaluation Depth Exceeded"))
Expand All @@ -345,7 +361,7 @@ func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult {
if r.FetchFromServer {
return r
}
exposures = append(exposures, r.SecondaryExposures...)
exposures = e.cleanExposures(append(exposures, r.SecondaryExposures...))
if r.Value {

delegatedResult := e.evalDelegate(user, rule, exposures, depth+1)
Expand Down Expand Up @@ -407,7 +423,7 @@ func (e *evaluator) evalDelegate(user User, rule configRule, exposures []map[str

result := e.eval(user, config, depth+1)
result.ConfigDelegate = rule.ConfigDelegate
result.SecondaryExposures = append(exposures, result.SecondaryExposures...)
result.SecondaryExposures = e.cleanExposures(append(exposures, result.SecondaryExposures...))
result.UndelegatedSecondaryExposures = exposures

explicitParams := map[string]bool{}
Expand Down

0 comments on commit 8285fec

Please sign in to comment.