Skip to content

Performance improvements #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: build-and-test
on:
workflow_dispatch:
push:
branches: [main]
branches: [main, performance-improvements]
pull_request:
branches: [main]
branches: [main, performance-improvements]

env:
COVERAGE: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name: KONG
on:
workflow_dispatch:
pull_request:
branches: [main]
branches: [main, performance-improvements]
push:
branches: [main]
branches: [main, performance-improvements]

env:
test_api_key: ${{ secrets.KONG_SERVER_SDK_KEY }}
Expand Down
128 changes: 128 additions & 0 deletions lib/api_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
require 'constants'

class UnsupportedConfigException < StandardError
end

module Statsig
class APIConfig
attr_accessor :name, :type, :is_active, :salt, :default_value, :enabled,
:rules, :id_type, :entity, :explicit_parameters, :has_shared_params, :target_app_ids

def initialize(name:, type:, is_active:, salt:, default_value:, enabled:, rules:, id_type:, entity:,
explicit_parameters: nil, has_shared_params: nil, target_app_ids: nil)
@name = name
@type = type.to_sym unless entity.nil?
@is_active = is_active
@salt = salt
@default_value = default_value
@enabled = enabled
@rules = rules
@id_type = id_type
@entity = entity.to_sym unless entity.nil?
@explicit_parameters = explicit_parameters
@has_shared_params = has_shared_params
@target_app_ids = target_app_ids
end

def self.from_json(json)
new(
name: json[:name],
type: json[:type],
is_active: json[:isActive],
salt: json[:salt],
default_value: json[:defaultValue],
enabled: json[:enabled],
rules: json[:rules]&.map do |rule|
APIRule.from_json(rule)
end,
id_type: json[:idType],
entity: json[:entity],
explicit_parameters: json[:explicitParameters],
has_shared_params: json[:hasSharedParams],
target_app_ids: json[:targetAppIDs]
)
end
end
end

module Statsig
class APIRule

attr_accessor :name, :pass_percentage, :return_value, :id, :salt,
:conditions, :id_type, :group_name, :config_delegate, :is_experiment_group

def initialize(name:, pass_percentage:, return_value:, id:, salt:, conditions:, id_type:,
group_name: nil, config_delegate: nil, is_experiment_group: nil)
@name = name
@pass_percentage = pass_percentage.to_f
@return_value = return_value
@id = id
@salt = salt
@conditions = conditions
@id_type = id_type
@group_name = group_name
@config_delegate = config_delegate
@is_experiment_group = is_experiment_group
end

def self.from_json(json)
new(
name: json[:name],
pass_percentage: json[:passPercentage],
return_value: json[:returnValue],
id: json[:id],
salt: json[:salt],
conditions: json[:conditions]&.map do |condition|
APICondition.from_json(condition)
end,
id_type: json[:idType],
group_name: json[:groupName],
config_delegate: json[:configDelegate],
is_experiment_group: json[:isExperimentGroup]
)
end
end
end

module Statsig
class APICondition

attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type

def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:)
@type = type.to_sym unless type.nil?
@target_value = target_value
@operator = operator.to_sym unless operator.nil?
@field = field
@additional_values = additional_values || {}
@id_type = id_type
end

def self.from_json(json)
operator = json[:operator]
unless operator.nil?
operator = operator&.downcase&.to_sym
unless Const::SUPPORTED_OPERATORS.include?(operator)
raise UnsupportedConfigException
end
end

type = json[:type]
unless type.nil?
type = type&.downcase&.to_sym
unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
raise UnsupportedConfigException
end
end

new(
type: json[:type],
target_value: json[:targetValue],
operator: json[:operator],
field: json[:field],
additional_values: json[:additionalValues],
id_type: json[:idType]
)
end
end
end
159 changes: 73 additions & 86 deletions lib/client_initialize_helpers.rb
Original file line number Diff line number Diff line change
@@ -1,148 +1,135 @@
# typed: true

require_relative 'hash_utils'
require 'sorbet-runtime'

require 'constants'

module ClientInitializeHelpers
module Statsig
class ResponseFormatter
extend T::Sig

def initialize(evaluator, user, hash, client_sdk_key)
@evaluator = evaluator
@user = user
@specs = evaluator.spec_store.get_raw_specs
@hash = hash
@client_sdk_key = client_sdk_key
end

def get_responses(key)
@specs[key]
.map { |name, spec| to_response(name, spec) }
.delete_if { |v| v.nil? }.to_h
end

private
def self.get_responses(entities, evaluator, user, client_sdk_key, hash_algo, include_exposures: true)
result = {}

sig { params(secondary_exposures: T::Array[T::Hash[String, String]]).returns(T::Array[T::Hash[String, String]]) }
def filter_segments_from_secondary_exposures(secondary_exposures)
secondary_exposures.reject do |exposure|
exposure['gate'].to_s.start_with?('segment:')
entities.each do |name, spec|
hashed_name, value = to_response(name, spec, evaluator, user, client_sdk_key, hash_algo, include_exposures)
if !hashed_name.nil? && !value.nil?
result[hashed_name] = value
end
end

result
end

def to_response(config_name, config_spec)
target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
config_target_apps = config_spec['targetAppIDs']
def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures)
target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
config_target_apps = config_spec.target_app_ids

unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
return nil
end

eval_result = @evaluator.eval_spec(@user, config_spec)
if eval_result.nil?
category = config_spec.type
entity_type = config_spec.entity
if entity_type == :segment || entity_type == :holdout
return nil
end

category = config_spec['type']
entity_type = config_spec['entity']
eval_result = ConfigResult.new(config_name, disable_evaluation_details: true)
evaluator.eval_spec(user, config_spec, eval_result)

result = {}

case category

when 'feature_gate'
if entity_type == 'segment' || entity_type == 'holdout'
return nil
end

result['value'] = eval_result.gate_value
result["group_name"] = eval_result.group_name
result["id_type"] = eval_result.id_type
when 'dynamic_config'
id_type = config_spec['idType']
result['value'] = eval_result.json_value
result["group"] = eval_result.rule_id
result["group_name"] = eval_result.group_name
result["id_type"] = eval_result.id_type
result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
when :feature_gate
result[:value] = eval_result.gate_value
result[:group_name] = eval_result.group_name
result[:id_type] = eval_result.id_type
when :dynamic_config
id_type = config_spec.id_type
result[:value] = eval_result.json_value
result[:group] = eval_result.rule_id
result[:group_name] = eval_result.group_name
result[:id_type] = eval_result.id_type
result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
else
return nil
end

if entity_type == 'experiment'
populate_experiment_fields(config_name, config_spec, eval_result, result)
if entity_type == :experiment
populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
end

if entity_type == :layer
populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
result.delete(:id_type) # not exposed for layer configs in /initialize
end

if entity_type == 'layer'
populate_layer_fields(config_spec, eval_result, result)
result.delete('id_type') # not exposed for layer configs in /initialize
hashed_name = hash_name(config_name, hash_algo)

result[:name] = hashed_name
result[:rule_id] = eval_result.rule_id

if include_exposures
result[:secondary_exposures] = clean_exposures(eval_result.secondary_exposures)
end

hashed_name = hash_name(config_name)
[hashed_name, result.merge(
{
"name" => hashed_name,
"rule_id" => eval_result.rule_id,
"secondary_exposures" => clean_exposures(eval_result.secondary_exposures)
}).compact]
[hashed_name, result]
end

def clean_exposures(exposures)
def self.clean_exposures(exposures)
seen = {}
exposures.reject do |exposure|
key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
key = "#{exposure[:gate]}|#{exposure[:gateValue]}|#{exposure[:ruleID]}}"
should_reject = seen[key]
seen[key] = true
should_reject == true
end
end

def populate_experiment_fields(config_name, config_spec, eval_result, result)
result["is_user_in_experiment"] = eval_result.is_experiment_group
result["is_experiment_active"] = config_spec['isActive'] == true
def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
result[:is_user_in_experiment] = eval_result.is_experiment_group
result[:is_experiment_active] = config_spec.is_active == true

if config_spec['hasSharedParams'] != true
if config_spec.has_shared_params != true
return
end

result["is_in_layer"] = true
result["explicit_parameters"] = config_spec["explicitParameters"] || []
result[:is_in_layer] = true
result[:explicit_parameters] = config_spec.explicit_parameters || []

layer_name = @specs[:experiment_to_layer][config_name]
if layer_name.nil? || @specs[:layers][layer_name].nil?
layer_name = evaluator.spec_store.experiment_to_layer[config_name]
if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
return
end

layer = @specs[:layers][layer_name]
result["value"] = layer["defaultValue"].merge(result["value"])
layer = evaluator.spec_store.layers[layer_name]
result[:value] = layer[:defaultValue].merge(result[:value])
end

def populate_layer_fields(config_spec, eval_result, result)
def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
delegate = eval_result.config_delegate
result["explicit_parameters"] = config_spec["explicitParameters"] || []
result[:explicit_parameters] = config_spec.explicit_parameters || []

if delegate.nil? == false && delegate.empty? == false
delegate_spec = @specs[:configs][delegate]
delegate_result = @evaluator.eval_spec(@user, delegate_spec)
delegate_spec = evaluator.spec_store.configs[delegate]

result["allocated_experiment_name"] = hash_name(delegate)
result["is_user_in_experiment"] = delegate_result.is_experiment_group
result["is_experiment_active"] = delegate_spec['isActive'] == true
result["explicit_parameters"] = delegate_spec["explicitParameters"] || []
result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
result[:is_user_in_experiment] = eval_result.is_experiment_group
result[:is_experiment_active] = delegate_spec.is_active == true
result[:explicit_parameters] = delegate_spec.explicit_parameters || []
end

result["undelegated_secondary_exposures"] = clean_exposures(eval_result.undelegated_sec_exps || [])
if include_exposures
result[:undelegated_secondary_exposures] = clean_exposures(eval_result.undelegated_sec_exps || [])
end
end

def hash_name(name)
case @hash
when 'none'
def self.hash_name(name, hash_algo)
case hash_algo
when Statsig::Const::NONE
return name
when 'sha256'
return Statsig::HashUtils.sha256(name)
when 'djb2'
when Statsig::Const::DJB2
return Statsig::HashUtils.djb2(name)
else
return Statsig::HashUtils.sha256(name)
end
end
end
Expand Down
Loading