From d61c0796992b2ca27443a04cf9dc3a772ca699a0 Mon Sep 17 00:00:00 2001 From: kat-statsig <167801639+kat-statsig@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:10:30 -0700 Subject: [PATCH] add hash algo + hash secondary and undelegated exposures (#209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![Screenshot 2024-07-22 at 1 01 11 PM](https://github.com/user-attachments/assets/c968ce8d-ce46-457a-8308-18c16f4836d8) --- client.go | 61 ++++++++++++++++--------- client_initialize_response.go | 19 ++++---- error_boundary.go | 19 +++----- evaluation_test.go | 6 +-- evaluator.go | 84 +++++++++++++++++------------------ logger.go | 11 ++--- statsig.go | 3 +- statsig_context.go | 22 +++++++++ util.go | 11 +++++ 9 files changed, 140 insertions(+), 96 deletions(-) create mode 100644 statsig_context.go diff --git a/client.go b/client.go index 9c98af5..8b91437 100644 --- a/client.go +++ b/client.go @@ -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 @@ -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) }) @@ -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 @@ -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) }) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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) } @@ -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")) } @@ -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} @@ -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) @@ -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) @@ -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) diff --git a/client_initialize_response.go b/client_initialize_response.go index de2b2eb..3965956 100644 --- a/client_initialize_response.go +++ b/client_initialize_response.go @@ -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, @@ -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{ @@ -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{ @@ -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, @@ -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) diff --git a/error_boundary.go b/error_boundary.go index 5f478ee..17ce9ad 100644 --- a/error_boundary.go +++ b/error_boundary.go @@ -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 } @@ -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 } @@ -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) @@ -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 { @@ -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{}) } diff --git a/evaluation_test.go b/evaluation_test.go index 42d3b80..cd41e0d 100644 --- a/evaluation_test.go +++ b/evaluation_test.go @@ -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) @@ -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) @@ -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) diff --git a/evaluator.go b/evaluator.go index 477555c..f53190d 100644 --- a/evaluator.go +++ b/evaluator.go @@ -136,16 +136,16 @@ func (e *evaluator) createEvaluationDetails(reason evaluationReason) *Evaluation return newEvaluationDetails(reason, e.store.lastSyncTime, e.store.initialSyncTime) } -func (e *evaluator) evalGate(user User, gateName string) *evalResult { - return e.evalGateImpl(user, gateName, 0) +func (e *evaluator) evalGate(user User, gateName string, context StatsigContext) *evalResult { + return e.evalGateImpl(user, gateName, 0, context) } -func (e *evaluator) evalGateImpl(user User, gateName string, depth int) *evalResult { +func (e *evaluator) evalGateImpl(user User, gateName string, depth int, context StatsigContext) *evalResult { if gateOverrideEval, hasOverride := e.getGateOverrideEval(gateName); hasOverride { return gateOverrideEval } if gate, hasGate := e.store.getGate(gateName); hasGate { - return e.eval(user, gate, depth) + return e.eval(user, gate, depth, context) } emptyEvalResult := new(evalResult) emptyEvalResult.EvaluationDetails = e.createEvaluationDetails(reasonUnrecognized) @@ -153,11 +153,11 @@ func (e *evaluator) evalGateImpl(user User, gateName string, depth int) *evalRes return emptyEvalResult } -func (e *evaluator) evalConfig(user User, configName string, persistedValues UserPersistedValues) *evalResult { - return e.evalConfigImpl(user, configName, persistedValues, 0) +func (e *evaluator) evalConfig(user User, configName string, persistedValues UserPersistedValues, context StatsigContext) *evalResult { + return e.evalConfigImpl(user, configName, persistedValues, 0, context) } -func (e *evaluator) evalConfigImpl(user User, configName string, persistedValues UserPersistedValues, depth int) *evalResult { +func (e *evaluator) evalConfigImpl(user User, configName string, persistedValues UserPersistedValues, depth int, context StatsigContext) *evalResult { if configOverrideEval, hasOverride := e.getConfigOverrideEval(configName); hasOverride { return configOverrideEval } @@ -170,7 +170,7 @@ func (e *evaluator) evalConfigImpl(user User, configName string, persistedValues } if persistedValues == nil || config.IsActive == nil || !*config.IsActive { - return e.evalAndDeleteFromPersistentStorage(user, config, depth) + return e.evalAndDeleteFromPersistentStorage(user, config, depth, context) } stickyResult := newEvalResultFromUserPersistedValues(configName, persistedValues) @@ -178,14 +178,14 @@ func (e *evaluator) evalConfigImpl(user User, configName string, persistedValues return stickyResult } - return e.evalAndSaveToPersistentStorage(user, config, depth) + return e.evalAndSaveToPersistentStorage(user, config, depth, context) } -func (e *evaluator) evalLayer(user User, name string, persistedValues UserPersistedValues) *evalResult { - return e.evalLayerImpl(user, name, persistedValues, 0) +func (e *evaluator) evalLayer(user User, name string, persistedValues UserPersistedValues, context StatsigContext) *evalResult { + return e.evalLayerImpl(user, name, persistedValues, 0, context) } -func (e *evaluator) evalLayerImpl(user User, name string, persistedValues UserPersistedValues, depth int) *evalResult { +func (e *evaluator) evalLayerImpl(user User, name string, persistedValues UserPersistedValues, depth int, context StatsigContext) *evalResult { if layerOverrideEval, hasOverride := e.getLayerOverrideEval(name); hasOverride { return layerOverrideEval } @@ -198,7 +198,7 @@ func (e *evaluator) evalLayerImpl(user User, name string, persistedValues UserPe } if persistedValues == nil { - return e.evalAndDeleteFromPersistentStorage(user, config, depth) + return e.evalAndDeleteFromPersistentStorage(user, config, depth, context) } stickyResult := newEvalResultFromUserPersistedValues(name, persistedValues) @@ -206,10 +206,10 @@ func (e *evaluator) evalLayerImpl(user User, name string, persistedValues UserPe if e.allocatedExperimentExistsAndIsActive(stickyResult) { return stickyResult } else { - return e.evalAndDeleteFromPersistentStorage(user, config, depth) + return e.evalAndDeleteFromPersistentStorage(user, config, depth, context) } } else { - evaluation := e.eval(user, config, depth) + evaluation := e.eval(user, config, depth, context) if e.allocatedExperimentExistsAndIsActive(evaluation) { if evaluation.IsExperimentGroup != nil && *evaluation.IsExperimentGroup { e.persistentStorageUtils.save(user, config.IDType, name, evaluation) @@ -226,17 +226,17 @@ func (e *evaluator) allocatedExperimentExistsAndIsActive(evaluation *evalResult) return exists && delegate.IsActive != nil && *delegate.IsActive } -func (e *evaluator) evalAndSaveToPersistentStorage(user User, config configSpec, depth int) *evalResult { - evaluation := e.eval(user, config, depth) +func (e *evaluator) evalAndSaveToPersistentStorage(user User, config configSpec, depth int, context StatsigContext) *evalResult { + evaluation := e.eval(user, config, depth, context) if evaluation.IsExperimentGroup != nil && *evaluation.IsExperimentGroup { e.persistentStorageUtils.save(user, config.IDType, config.Name, evaluation) } return evaluation } -func (e *evaluator) evalAndDeleteFromPersistentStorage(user User, config configSpec, depth int) *evalResult { +func (e *evaluator) evalAndDeleteFromPersistentStorage(user User, config configSpec, depth int, context StatsigContext) *evalResult { e.persistentStorageUtils.delete(user, config.IDType, config.Name) - return e.eval(user, config, depth) + return e.eval(user, config, depth, context) } func (e *evaluator) getGateOverride(name string) (bool, bool) { @@ -327,17 +327,14 @@ func (e *evaluator) OverrideLayer(layer string, val map[string]interface{}) { // Gets all evaluated values for the given user. // These values can then be given to a Statsig Client SDK via bootstrapping. -func (e *evaluator) getClientInitializeResponse(user User, clientKey string, includeLocalOverrides bool) ClientInitializeResponse { - return getClientInitializeResponse(user, e, clientKey, includeLocalOverrides) +func (e *evaluator) getClientInitializeResponse(user User, clientKey string, includeLocalOverrides bool, hashAlgorithm string) ClientInitializeResponse { + return getClientInitializeResponse(user, e, clientKey, includeLocalOverrides, hashAlgorithm) } -func (e *evaluator) cleanExposures(exposures []SecondaryExposure) []SecondaryExposure { +func (e *evaluator) cleanExposures(exposures []SecondaryExposure, hashAlgorithm string) []SecondaryExposure { seen := make(map[string]bool) result := make([]SecondaryExposure, 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 @@ -347,7 +344,7 @@ func (e *evaluator) cleanExposures(exposures []SecondaryExposure) []SecondaryExp return result } -func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult { +func (e *evaluator) eval(user User, spec configSpec, depth int, context StatsigContext) *evalResult { if depth > maxRecursiveDepth { panic(errors.New("Statsig Evaluation Depth Exceeded")) } @@ -365,14 +362,13 @@ func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult { defaultRuleID := "default" if spec.Enabled { for _, rule := range spec.Rules { - r := e.evalRule(user, rule, depth+1) + r := e.evalRule(user, rule, depth+1, context) if r.FetchFromServer { return r } - exposures = e.cleanExposures(append(exposures, r.SecondaryExposures...)) + exposures = e.cleanExposures(append(exposures, r.SecondaryExposures...), context.Hash) if r.Value { - - delegatedResult := e.evalDelegate(user, rule, exposures, depth+1) + delegatedResult := e.evalDelegate(user, rule, exposures, depth+1, context) if delegatedResult != nil { return delegatedResult } @@ -423,15 +419,15 @@ func (e *evaluator) eval(user User, spec configSpec, depth int) *evalResult { return &evalResult{Value: false, RuleID: defaultRuleID, SecondaryExposures: exposures} } -func (e *evaluator) evalDelegate(user User, rule configRule, exposures []SecondaryExposure, depth int) *evalResult { +func (e *evaluator) evalDelegate(user User, rule configRule, exposures []SecondaryExposure, depth int, context StatsigContext) *evalResult { config, hasConfig := e.store.getDynamicConfig(rule.ConfigDelegate) if !hasConfig { return nil } - result := e.eval(user, config, depth+1) + result := e.eval(user, config, depth+1, context) result.ConfigDelegate = rule.ConfigDelegate - result.SecondaryExposures = e.cleanExposures(append(exposures, result.SecondaryExposures...)) + result.SecondaryExposures = e.cleanExposures(append(exposures, result.SecondaryExposures...), context.Hash) result.UndelegatedSecondaryExposures = exposures explicitParams := map[string]bool{} @@ -465,11 +461,11 @@ func getUnitID(user User, idType string) string { return user.UserID } -func (e *evaluator) evalRule(user User, rule configRule, depth int) *evalResult { +func (e *evaluator) evalRule(user User, rule configRule, depth int, context StatsigContext) *evalResult { var exposures = make([]SecondaryExposure, 0) var finalResult = &evalResult{Value: true, FetchFromServer: false} for _, cond := range rule.Conditions { - res := e.evalCondition(user, cond, depth+1) + res := e.evalCondition(user, cond, depth+1, context) if !res.Value { finalResult.Value = false } @@ -482,7 +478,7 @@ func (e *evaluator) evalRule(user User, rule configRule, depth int) *evalResult return finalResult } -func (e *evaluator) evalCondition(user User, cond configCondition, depth int) *evalResult { +func (e *evaluator) evalCondition(user User, cond configCondition, depth int, context StatsigContext) *evalResult { var value interface{} condType := strings.ToLower(cond.Type) op := strings.ToLower(cond.Operator) @@ -494,16 +490,20 @@ func (e *evaluator) evalCondition(user User, cond configCondition, depth int) *e if !ok { return &evalResult{Value: false} } - result := e.evalGateImpl(user, dependentGateName, depth+1) + result := e.evalGateImpl(user, dependentGateName, depth+1, context) if result.FetchFromServer { return &evalResult{FetchFromServer: true} } - newExposure := SecondaryExposure{ - Gate: dependentGateName, - GateValue: strconv.FormatBool(result.Value), - RuleID: result.RuleID, + allExposures := result.SecondaryExposures + if !strings.HasPrefix(dependentGateName, "segment:") { + newExposure := SecondaryExposure{ + Gate: hashName(context.Hash, dependentGateName), + GateValue: strconv.FormatBool(result.Value), + RuleID: result.RuleID, + } + allExposures = append(result.SecondaryExposures, newExposure) } - allExposures := append(result.SecondaryExposures, newExposure) + if condType == "pass_gate" { return &evalResult{Value: result.Value, SecondaryExposures: allExposures} } else { diff --git a/logger.go b/logger.go index 5476f54..6f6b6ff 100644 --- a/logger.go +++ b/logger.go @@ -286,12 +286,9 @@ func (l *logger) sendEvents(events []interface{}) { var res logEventResponse _, err := l.transport.log_event(events, &res, RequestOptions{retries: maxRetries}) if err != nil { - extra := map[string]interface{}{ - "eventCount": len(events), - } - options := logExceptionOptions{ - Tag: "statsig::log_event_failed", - Extra: extra, + context := StatsigContext{ + Caller: "statsig::log_event_failed", + EventCount: len(events), BypassDedupe: true, LogToOutput: true, } @@ -299,7 +296,7 @@ func (l *logger) sendEvents(events []interface{}) { Events: len(events), Err: err, } - l.errorBoundary.logExceptionWithOptions(err, options) + l.errorBoundary.logExceptionWithContext(err, context) } } diff --git a/statsig.go b/statsig.go index a06cdaa..d586bb9 100644 --- a/statsig.go +++ b/statsig.go @@ -95,6 +95,7 @@ type Environment struct { type GCIROptions struct { IncludeLocalOverrides bool ClientKey string + HashAlgorithm string } // IsInitialized returns whether the global Statsig instance has already been initialized or not @@ -270,7 +271,7 @@ func GetLayerWithOptions(user User, layer string, options *GetLayerOptions) Laye if !IsInitialized() { panic(fmt.Errorf("must Initialize() statsig before calling GetLayerWithOptions")) } - return instance.getLayerImpl(user, layer, options) + return instance.getLayerImpl(user, layer, options, StatsigContext{Caller: "GetLayerWithOptions", ConfigName: layer}) } // Logs an exposure event for the parameter in the given layer diff --git a/statsig_context.go b/statsig_context.go new file mode 100644 index 0000000..0dcf90a --- /dev/null +++ b/statsig_context.go @@ -0,0 +1,22 @@ +package statsig + +type StatsigContext struct { + Caller string + EventCount int + ConfigName string + ClientKey string + Hash string + BypassDedupe bool + TargetAppID string + LogToOutput bool +} + +func (sc *StatsigContext) getContextForLogging() map[string]interface{} { + return map[string]interface{}{ + "tag": sc.Caller, + "eventCount": sc.EventCount, + "configName": sc.ConfigName, + "clientKey": sc.ClientKey, + "hash": sc.ClientKey, + } +} diff --git a/util.go b/util.go index e30e73e..cb9e5f8 100644 --- a/util.go +++ b/util.go @@ -115,3 +115,14 @@ func getUnixMilli() int64 { unixNano := time.Now().UnixNano() return unixNano / int64(time.Millisecond) } + +func hashName(hashAlgorithm string, name string) string { + switch hashAlgorithm { + case "sha256": + return getHashBase64StringEncoding(name) + case "djb2": + return getDJB2Hash(name) + default: + return name + } +}