Skip to content

Commit

Permalink
Add derived device metadata to metadata (#228)
Browse files Browse the repository at this point in the history
adding derived device metadata to metadata for scrapi to use. There are
discrepancies between different ua parsers, so log event processor will
add a different browser/os information to the exposure stream than what
the sdk has parsed out, which is misleading to users who are looking at
the exposure stream to determine how to set their rules.
  • Loading branch information
kat-statsig authored Sep 18, 2024
1 parent a94ce8d commit 4e57bd9
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 49 deletions.
12 changes: 6 additions & 6 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (c *Client) ManuallyLogGateExposure(user User, gate string) {
user = normalizeUser(user, *c.options)
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)
c.logger.logGateExposure(user, gate, res, context)
})
}

Expand Down Expand Up @@ -147,7 +147,7 @@ func (c *Client) ManuallyLogConfigExposure(user User, config string) {
user = normalizeUser(user, *c.options)
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)
c.logger.logConfigExposure(user, config, res, context)
})
}

Expand Down Expand Up @@ -236,7 +236,7 @@ func (c *Client) ManuallyLogLayerParameterExposure(user User, layer string, para
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)
c.logger.logLayerExposure(user, *config, parameter, *res, context)
})
}

Expand Down Expand Up @@ -378,7 +378,7 @@ func (c *Client) checkGateImpl(user User, name string, options checkGateOptions,
res = &evalResult{Value: serverRes.Value, RuleID: serverRes.RuleID}
} else {
context := &logContext{isManualExposure: false}
exposure := c.logger.getGateExposureWithEvaluationDetails(user, name, res.Value, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
exposure := c.logger.getGateExposureWithEvaluationDetails(user, name, res, context)
if !options.disableLogExposures {
c.logger.logExposure(*exposure)
}
Expand Down Expand Up @@ -432,7 +432,7 @@ func (c *Client) getConfigImpl(user User, name string, context getConfigImplCont
logExposure = !context.configOptions.disableLogExposures
}
context := &logContext{isManualExposure: false}
exposure := c.logger.getConfigExposureWithEvaluationDetails(user, name, res.RuleID, res.SecondaryExposures, res.EvaluationDetails, context)
exposure := c.logger.getConfigExposureWithEvaluationDetails(user, name, res, context)
if logExposure {
c.logger.logExposure(*exposure)
}
Expand Down Expand Up @@ -478,7 +478,7 @@ func (c *Client) getLayerImpl(user User, name string, options *GetLayerOptions,

logFunc := func(layer Layer, parameterName string) {
context := &logContext{isManualExposure: false}
exposure := c.logger.getLayerExposureWithEvaluationDetails(user, layer, parameterName, *res, res.EvaluationDetails, context)
exposure := c.logger.getLayerExposureWithEvaluationDetails(user, layer, parameterName, *res, context)
if !options.DisableLogExposures {
c.logger.logExposure(*exposure)
}
Expand Down
154 changes: 154 additions & 0 deletions download_config_specs_ua_gates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
{
"dynamic_configs": [],
"feature_gates": [
{
"name": "test_ua_browser_name",
"type": "feature_gate",
"salt": "30c76215-8a9b-461d-bcc0-a99197534d52",
"enabled": true,
"defaultValue": false,
"rules": [
{
"name": "9eiCEHlnoUMZM297goyGh",
"passPercentage": 100,
"conditions": [
{
"type": "ua_based",
"targetValue": [
"Chrome"
],
"operator": "any",
"field": "browser_name",
"additionalValues": {},
"isDeviceBased": false,
"idType": "userID"
}
],
"returnValue": true,
"id": "1gGkMDmjbL66xC9STQnz64:100.00:4",
"salt": "a68db427-b46e-4507-93e4-0abda62c4c74",
"isDeviceBased": false,
"idType": "userID"
}
],
"isDeviceBased": false,
"idType": "userID",
"entity": "feature_gate"
},
{
"name": "test_ua_browser_version",
"type": "feature_gate",
"salt": "30c76215-8a9b-461d-bcc0-a99197534d52",
"enabled": true,
"defaultValue": false,
"rules": [
{
"name": "9eiCEHlnoUMZM297goyGh",
"passPercentage": 100,
"conditions": [
{
"type": "ua_based",
"targetValue": [
"58.0.3029.110"
],
"operator": "any",
"field": "browser_version",
"additionalValues": {},
"isDeviceBased": false,
"idType": "userID"
}
],
"returnValue": true,
"id": "1gGkMDmjbL66xC9STQnz64:100.00:4",
"salt": "a68db427-b46e-4507-93e4-0abda62c4c74",
"isDeviceBased": false,
"idType": "userID"
}
],
"isDeviceBased": false,
"idType": "userID",
"entity": "feature_gate"
},
{
"name": "test_ua_os_name",
"type": "feature_gate",
"salt": "30c76215-8a9b-461d-bcc0-a99197534d52",
"enabled": true,
"defaultValue": false,
"rules": [
{
"name": "9eiCEHlnoUMZM297goyGh",
"passPercentage": 100,
"conditions": [
{
"type": "ua_based",
"targetValue": [
"Windows"
],
"operator": "any",
"field": "os_name",
"additionalValues": {},
"isDeviceBased": false,
"idType": "userID"
}
],
"returnValue": true,
"id": "1gGkMDmjbL66xC9STQnz64:100.00:4",
"salt": "a68db427-b46e-4507-93e4-0abda62c4c74",
"isDeviceBased": false,
"idType": "userID"
}
],
"isDeviceBased": false,
"idType": "userID",
"entity": "feature_gate"
},
{
"name": "test_ua_os_version",
"type": "feature_gate",
"salt": "30c76215-8a9b-461d-bcc0-a99197534d52",
"enabled": true,
"defaultValue": false,
"rules": [
{
"name": "9eiCEHlnoUMZM297goyGh",
"passPercentage": 100,
"conditions": [
{
"type": "ua_based",
"targetValue": [
"10"
],
"operator": "any",
"field": "os_version",
"additionalValues": {},
"isDeviceBased": false,
"idType": "userID"
}
],
"returnValue": true,
"id": "1gGkMDmjbL66xC9STQnz64:100.00:4",
"salt": "a68db427-b46e-4507-93e4-0abda62c4c74",
"isDeviceBased": false,
"idType": "userID"
}
],
"isDeviceBased": false,
"idType": "userID",
"entity": "feature_gate"
}
],
"layer_configs": [],
"layers": {},
"has_updates": true,
"time": 1631638014811,
"id_lists": {},
"diagnostics": {
"download_config_specs": 10000,
"get_id_list": 10000,
"get_id_list_sources": 10000,
"api_call": 10000,
"config_sync": 10000,
"initialize": 10000
}
}
73 changes: 60 additions & 13 deletions evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ type evalResult struct {
ExplicitParameters []string `json:"explicit_parameters"`
EvaluationDetails *EvaluationDetails `json:"evaluation_details,omitempty"`
IsExperimentGroup *bool `json:"is_experiment_group,omitempty"`
DerivedDeviceMetadata *DerivedDeviceMetadata `json:"derived_device_metadata,omitempty"`
}

type DerivedDeviceMetadata struct {
OsName string `json:"os_name"`
OsVersion string `json:"os_version"`
BrowserName string `json:"browser_name"`
BrowserVersion string `json:"browser_version"`
}

type SecondaryExposure struct {
Expand Down Expand Up @@ -363,13 +371,16 @@ func (e *evaluator) eval(user User, spec configSpec, depth int, context StatsigC

var exposures = make([]SecondaryExposure, 0)
defaultRuleID := "default"
var deviceMetadata *DerivedDeviceMetadata

if spec.Enabled {
for _, rule := range spec.Rules {
r := e.evalRule(user, rule, depth+1, context)
if r.FetchFromServer {
return r
}
exposures = e.cleanExposures(append(exposures, r.SecondaryExposures...))
deviceMetadata = assignDerivedDeviceMetadata(r, deviceMetadata)
if r.Value {
delegatedResult := e.evalDelegate(user, rule, exposures, depth+1, context)
if delegatedResult != nil {
Expand All @@ -389,18 +400,20 @@ func (e *evaluator) eval(user User, spec configSpec, depth int, context StatsigC
SecondaryExposures: exposures,
UndelegatedSecondaryExposures: exposures,
EvaluationDetails: evalDetails,
DerivedDeviceMetadata: deviceMetadata,
}
if rule.IsExperimentGroup != nil {
result.IsExperimentGroup = rule.IsExperimentGroup
}
return result
} else {
return &evalResult{
Value: pass,
RuleID: rule.ID,
GroupName: rule.GroupName,
SecondaryExposures: exposures,
EvaluationDetails: evalDetails,
Value: pass,
RuleID: rule.ID,
GroupName: rule.GroupName,
SecondaryExposures: exposures,
EvaluationDetails: evalDetails,
DerivedDeviceMetadata: deviceMetadata,
}
}
}
Expand All @@ -417,9 +430,10 @@ func (e *evaluator) eval(user User, spec configSpec, depth int, context StatsigC
SecondaryExposures: exposures,
UndelegatedSecondaryExposures: exposures,
EvaluationDetails: evalDetails,
DerivedDeviceMetadata: deviceMetadata,
}
}
return &evalResult{Value: false, RuleID: defaultRuleID, SecondaryExposures: exposures}
return &evalResult{Value: false, RuleID: defaultRuleID, SecondaryExposures: exposures, DerivedDeviceMetadata: deviceMetadata}
}

func (e *evaluator) evalDelegate(user User, rule configRule, exposures []SecondaryExposure, depth int, context StatsigContext) *evalResult {
Expand Down Expand Up @@ -461,6 +475,7 @@ func getUnitID(user User, idType string) string {

func (e *evaluator) evalRule(user User, rule configRule, depth int, context StatsigContext) *evalResult {
var exposures = make([]SecondaryExposure, 0)
var deviceMetadata *DerivedDeviceMetadata
var finalResult = &evalResult{Value: true, FetchFromServer: false}
for _, cond := range rule.Conditions {
res := e.evalCondition(user, cond, depth+1, context)
Expand All @@ -470,16 +485,20 @@ func (e *evaluator) evalRule(user User, rule configRule, depth int, context Stat
if res.FetchFromServer {
finalResult.FetchFromServer = true
}
deviceMetadata = assignDerivedDeviceMetadata(res, deviceMetadata)
exposures = append(exposures, res.SecondaryExposures...)
}
finalResult.SecondaryExposures = exposures
finalResult.DerivedDeviceMetadata = deviceMetadata
return finalResult
}

func (e *evaluator) evalCondition(user User, cond configCondition, depth int, context StatsigContext) *evalResult {
var value interface{}
condType := cond.Type
op := cond.Operator
var deviceMetadata *DerivedDeviceMetadata

switch {
case strings.EqualFold(condType, "public"):
return &evalResult{Value: true}
Expand All @@ -504,9 +523,9 @@ func (e *evaluator) evalCondition(user User, cond configCondition, depth int, co
}

if strings.EqualFold(condType, "pass_gate") {
return &evalResult{Value: result.Value, SecondaryExposures: allExposures}
return &evalResult{Value: result.Value, SecondaryExposures: allExposures, DerivedDeviceMetadata: result.DerivedDeviceMetadata}
} else {
return &evalResult{Value: !result.Value, SecondaryExposures: allExposures}
return &evalResult{Value: !result.Value, SecondaryExposures: allExposures, DerivedDeviceMetadata: result.DerivedDeviceMetadata}
}
case strings.EqualFold(condType, "ip_based"):
value = getFromUser(user, cond.Field)
Expand All @@ -516,7 +535,8 @@ func (e *evaluator) evalCondition(user User, cond configCondition, depth int, co
case strings.EqualFold(condType, "ua_based"):
value = getFromUser(user, cond.Field)
if value == nil || value == "" {
value = getFromUserAgent(user, cond.Field, e.uaParser)
deviceMetadata = &DerivedDeviceMetadata{}
value = getFromUserAgent(user, cond.Field, e.uaParser, deviceMetadata)
}
case strings.EqualFold(condType, "user_field"):
value = getFromUser(user, cond.Field)
Expand Down Expand Up @@ -651,7 +671,7 @@ func (e *evaluator) evalCondition(user User, cond configCondition, depth int, co
pass = false
server = true
}
return &evalResult{Value: pass, FetchFromServer: server}
return &evalResult{Value: pass, FetchFromServer: server, DerivedDeviceMetadata: deviceMetadata}
}

func getFromUser(user User, field string) interface{} {
Expand Down Expand Up @@ -703,7 +723,7 @@ func getFromEnvironment(user User, field string) string {
return value
}

func getFromUserAgent(user User, field string, parser *uaParser) string {
func getFromUserAgent(user User, field string, parser *uaParser, deviceMetadata *DerivedDeviceMetadata) string {
ua := getFromUser(user, "useragent")
uaStr, ok := ua.(string)
if !ok {
Expand All @@ -715,13 +735,27 @@ func getFromUserAgent(user User, field string, parser *uaParser) string {
}
switch {
case strings.EqualFold(field, "os_name") || strings.EqualFold(field, "osname"):
if deviceMetadata != nil {
deviceMetadata.OsName = client.Os.Family
}
return client.Os.Family
case strings.EqualFold(field, "os_version") || strings.EqualFold(field, "osversion"):
return strings.Join(removeEmptyStrings([]string{client.Os.Major, client.Os.Minor, client.Os.Patch, client.Os.PatchMinor}), ".")
osVersion := strings.Join(removeEmptyStrings([]string{client.Os.Major, client.Os.Minor, client.Os.Patch, client.Os.PatchMinor}), ".")
if deviceMetadata != nil {
deviceMetadata.OsVersion = osVersion
}
return osVersion
case strings.EqualFold(field, "browser_name") || strings.EqualFold(field, "browsername"):
if deviceMetadata != nil {
deviceMetadata.BrowserName = client.UserAgent.Family
}
return client.UserAgent.Family
case strings.EqualFold(field, "browser_version") || strings.EqualFold(field, "browserversion"):
return strings.Join(removeEmptyStrings([]string{client.UserAgent.Major, client.UserAgent.Minor, client.UserAgent.Patch}), ".")
browserVersion := strings.Join(removeEmptyStrings([]string{client.UserAgent.Major, client.UserAgent.Minor, client.UserAgent.Patch}), ".")
if deviceMetadata != nil {
deviceMetadata.BrowserVersion = browserVersion
}
return browserVersion
}
return ""
}
Expand Down Expand Up @@ -953,3 +987,16 @@ func getUnixTimestamp(v interface{}) int64 {
}
return 0
}

func assignDerivedDeviceMetadata(res *evalResult, deviceMetadata *DerivedDeviceMetadata) *DerivedDeviceMetadata {
if res.DerivedDeviceMetadata != nil {
if deviceMetadata == nil {
deviceMetadata = &DerivedDeviceMetadata{}
}
deviceMetadata.OsName = res.DerivedDeviceMetadata.OsName
deviceMetadata.OsVersion = res.DerivedDeviceMetadata.OsVersion
deviceMetadata.BrowserName = res.DerivedDeviceMetadata.BrowserName
deviceMetadata.BrowserVersion = res.DerivedDeviceMetadata.BrowserVersion
}
return deviceMetadata
}
Loading

0 comments on commit 4e57bd9

Please sign in to comment.