statsig 1.34.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/api_config.rb +0 -141
- data/lib/client_initialize_helpers.rb +26 -27
- data/lib/constants.rb +65 -0
- data/lib/dynamic_config.rb +13 -5
- data/lib/evaluation_helpers.rb +16 -5
- data/lib/evaluator.rb +146 -116
- data/lib/interfaces/data_store.rb +1 -1
- data/lib/layer.rb +12 -5
- data/lib/memo.rb +8 -6
- data/lib/network.rb +41 -28
- data/lib/spec_store.rb +39 -47
- data/lib/statsig.rb +1 -1
- data/lib/statsig_driver.rb +11 -6
- data/lib/statsig_options.rb +19 -10
- metadata +16 -3
- data/lib/uri_helper.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27afba91b3e46ea8661d399ef0dc2696c6372bb214dbb993ea986e5b456edb7e
|
4
|
+
data.tar.gz: c06043637e4ce2ae3bbdef95ccc8a0713717b04e8149b46a0f70da502aa729db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac429df53886c94abbd1b801adb96a8bab92e6c8c73aa4f910a2a505b5a3bf65ba3d3b844cca6014f6dbac0b17ce05d7a967d5fde260f0bc6f3021e60c84e8af
|
7
|
+
data.tar.gz: 14d9c181b6093d56ca7bba00873ff1806cc063ac43608d9ff3cde9e2396423faf58fe50b4096ac4faf7806bcfaec4c946c325f9be20a83b4f311e521162958bb
|
data/lib/api_config.rb
CHANGED
@@ -2,144 +2,3 @@ require 'constants'
|
|
2
2
|
|
3
3
|
class UnsupportedConfigException < StandardError
|
4
4
|
end
|
5
|
-
|
6
|
-
module Statsig
|
7
|
-
class APIConfig
|
8
|
-
attr_accessor :name, :type, :is_active, :salt, :default_value, :enabled,
|
9
|
-
:rules, :id_type, :entity, :explicit_parameters, :has_shared_params, :target_app_ids
|
10
|
-
|
11
|
-
def self.from_json(json)
|
12
|
-
new(
|
13
|
-
name: json[:name],
|
14
|
-
type: json[:type],
|
15
|
-
is_active: json[:isActive],
|
16
|
-
salt: json[:salt],
|
17
|
-
default_value: json[:defaultValue] || {},
|
18
|
-
enabled: json[:enabled],
|
19
|
-
rules: json[:rules]&.map do |rule|
|
20
|
-
APIRule.from_json(rule)
|
21
|
-
end,
|
22
|
-
id_type: json[:idType],
|
23
|
-
entity: json[:entity],
|
24
|
-
explicit_parameters: json[:explicitParameters],
|
25
|
-
has_shared_params: json[:hasSharedParams],
|
26
|
-
target_app_ids: json[:targetAppIDs]
|
27
|
-
)
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def initialize(name:, type:, is_active:, salt:, default_value:, enabled:, rules:, id_type:, entity:,
|
33
|
-
explicit_parameters: nil, has_shared_params: nil, target_app_ids: nil)
|
34
|
-
@name = name
|
35
|
-
@type = type.to_sym unless type.nil?
|
36
|
-
@is_active = is_active
|
37
|
-
@salt = salt
|
38
|
-
@default_value = JSON.parse(JSON.generate(default_value))
|
39
|
-
@enabled = enabled
|
40
|
-
@rules = rules
|
41
|
-
@id_type = id_type
|
42
|
-
@entity = entity.to_sym unless entity.nil?
|
43
|
-
@explicit_parameters = explicit_parameters
|
44
|
-
@has_shared_params = has_shared_params
|
45
|
-
@target_app_ids = target_app_ids
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
class APIRule
|
50
|
-
|
51
|
-
attr_accessor :name, :pass_percentage, :return_value, :id, :salt,
|
52
|
-
:conditions, :id_type, :group_name, :config_delegate, :is_experiment_group
|
53
|
-
|
54
|
-
def self.from_json(json)
|
55
|
-
new(
|
56
|
-
name: json[:name],
|
57
|
-
pass_percentage: json[:passPercentage],
|
58
|
-
return_value: json[:returnValue] || {},
|
59
|
-
id: json[:id],
|
60
|
-
salt: json[:salt],
|
61
|
-
conditions: json[:conditions]&.map do |condition|
|
62
|
-
APICondition.from_json(condition)
|
63
|
-
end,
|
64
|
-
id_type: json[:idType],
|
65
|
-
group_name: json[:groupName],
|
66
|
-
config_delegate: json[:configDelegate],
|
67
|
-
is_experiment_group: json[:isExperimentGroup]
|
68
|
-
)
|
69
|
-
end
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
def initialize(name:, pass_percentage:, return_value:, id:, salt:, conditions:, id_type:,
|
74
|
-
group_name: nil, config_delegate: nil, is_experiment_group: nil)
|
75
|
-
@name = name
|
76
|
-
@pass_percentage = pass_percentage.to_f
|
77
|
-
@return_value = JSON.parse(JSON.generate(return_value))
|
78
|
-
@id = id
|
79
|
-
@salt = salt
|
80
|
-
@conditions = conditions
|
81
|
-
@id_type = id_type
|
82
|
-
@group_name = group_name
|
83
|
-
@config_delegate = config_delegate
|
84
|
-
@is_experiment_group = is_experiment_group
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
class APICondition
|
89
|
-
|
90
|
-
attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type, :hash
|
91
|
-
def self.from_json(json)
|
92
|
-
hash = Statsig::HashUtils.md5(json.to_s).to_sym
|
93
|
-
return Statsig::Memo.for_global(:api_condition_from_json, hash) do
|
94
|
-
operator = json[:operator]
|
95
|
-
unless operator.nil?
|
96
|
-
operator = operator&.downcase&.to_sym
|
97
|
-
unless Const::SUPPORTED_OPERATORS.include?(operator)
|
98
|
-
raise UnsupportedConfigException
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
type = json[:type]
|
103
|
-
unless type.nil?
|
104
|
-
type = type&.downcase&.to_sym
|
105
|
-
unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
|
106
|
-
raise UnsupportedConfigException
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
new(
|
111
|
-
type: json[:type],
|
112
|
-
target_value: json[:targetValue],
|
113
|
-
operator: json[:operator],
|
114
|
-
field: json[:field],
|
115
|
-
additional_values: json[:additionalValues],
|
116
|
-
id_type: json[:idType],
|
117
|
-
hash: hash
|
118
|
-
)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
private
|
123
|
-
|
124
|
-
def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:, hash:)
|
125
|
-
@hash = hash
|
126
|
-
|
127
|
-
@type = type.to_sym unless type.nil?
|
128
|
-
if operator == "any_case_sensitive" || operator == "none_case_sensitive"
|
129
|
-
if target_value.is_a?(Array)
|
130
|
-
target_value = target_value.map { |item| [item.to_s, true] }.to_h
|
131
|
-
end
|
132
|
-
end
|
133
|
-
if operator == "any" || operator == "none"
|
134
|
-
if target_value.is_a?(Array)
|
135
|
-
target_value = target_value.map { |item| [item.to_s.downcase, true] }.to_h
|
136
|
-
end
|
137
|
-
end
|
138
|
-
@target_value = target_value
|
139
|
-
@operator = operator.to_sym unless operator.nil?
|
140
|
-
@field = field
|
141
|
-
@additional_values = additional_values || {}
|
142
|
-
@id_type = id_type
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
@@ -14,7 +14,13 @@ module Statsig
|
|
14
14
|
include_local_overrides: false
|
15
15
|
)
|
16
16
|
result = {}
|
17
|
+
target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
|
17
18
|
entities.each do |name, spec|
|
19
|
+
config_target_apps = spec[:targetAppIDs]
|
20
|
+
|
21
|
+
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
22
|
+
next
|
23
|
+
end
|
18
24
|
hashed_name, value = to_response(name, spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
|
19
25
|
if !hashed_name.nil? && !value.nil?
|
20
26
|
result[hashed_name] = value
|
@@ -25,25 +31,18 @@ module Statsig
|
|
25
31
|
end
|
26
32
|
|
27
33
|
def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
33
|
-
return nil
|
34
|
-
end
|
35
|
-
|
36
|
-
category = config_spec.type
|
37
|
-
entity_type = config_spec.entity
|
38
|
-
if entity_type == :segment || entity_type == :holdout
|
34
|
+
config_name_str = config_name.to_s
|
35
|
+
category = config_spec[:type]
|
36
|
+
entity_type = config_spec[:entity]
|
37
|
+
if entity_type == Const::TYPE_SEGMENT || entity_type == Const::TYPE_HOLDOUT
|
39
38
|
return nil
|
40
39
|
end
|
41
40
|
|
42
41
|
if include_local_overrides
|
43
42
|
case category
|
44
|
-
when
|
43
|
+
when Const::TYPE_FEATURE_GATE
|
45
44
|
local_override = evaluator.lookup_gate_override(config_name)
|
46
|
-
when
|
45
|
+
when Const::TYPE_DYNAMIC_CONFIG
|
47
46
|
local_override = evaluator.lookup_config_override(config_name)
|
48
47
|
end
|
49
48
|
end
|
@@ -54,7 +53,7 @@ module Statsig
|
|
54
53
|
disable_evaluation_details: true,
|
55
54
|
disable_exposures: !include_exposures
|
56
55
|
)
|
57
|
-
evaluator.eval_spec(user, config_spec, eval_result)
|
56
|
+
evaluator.eval_spec(config_name_str, user, config_spec, eval_result)
|
58
57
|
else
|
59
58
|
eval_result = local_override
|
60
59
|
end
|
@@ -67,10 +66,10 @@ module Statsig
|
|
67
66
|
end
|
68
67
|
|
69
68
|
case category
|
70
|
-
when
|
69
|
+
when Const::TYPE_FEATURE_GATE
|
71
70
|
result[:value] = eval_result.gate_value
|
72
|
-
when
|
73
|
-
id_type = config_spec
|
71
|
+
when Const::TYPE_DYNAMIC_CONFIG
|
72
|
+
id_type = config_spec[:idType]
|
74
73
|
result[:value] = eval_result.json_value
|
75
74
|
result[:group] = eval_result.rule_id
|
76
75
|
result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
|
@@ -78,16 +77,16 @@ module Statsig
|
|
78
77
|
return nil
|
79
78
|
end
|
80
79
|
|
81
|
-
if entity_type ==
|
80
|
+
if entity_type == Const::TYPE_EXPERIMENT
|
82
81
|
populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
|
83
82
|
end
|
84
83
|
|
85
|
-
if entity_type ==
|
84
|
+
if entity_type == Const::TYPE_LAYER
|
86
85
|
populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
87
86
|
result.delete(:id_type) # not exposed for layer configs in /initialize
|
88
87
|
end
|
89
88
|
|
90
|
-
hashed_name = hash_name(
|
89
|
+
hashed_name = hash_name(config_name_str, hash_algo)
|
91
90
|
|
92
91
|
result[:name] = hashed_name
|
93
92
|
result[:rule_id] = eval_result.rule_id
|
@@ -101,14 +100,14 @@ module Statsig
|
|
101
100
|
|
102
101
|
def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
|
103
102
|
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
104
|
-
result[:is_experiment_active] = config_spec
|
103
|
+
result[:is_experiment_active] = config_spec[:isActive] == true
|
105
104
|
|
106
|
-
if config_spec
|
105
|
+
if config_spec[:hasSharedParams] != true
|
107
106
|
return
|
108
107
|
end
|
109
108
|
|
110
109
|
result[:is_in_layer] = true
|
111
|
-
result[:explicit_parameters] = config_spec
|
110
|
+
result[:explicit_parameters] = config_spec[:explicitParameters] || []
|
112
111
|
|
113
112
|
layer_name = evaluator.spec_store.experiment_to_layer[config_name]
|
114
113
|
if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
|
@@ -121,15 +120,15 @@ module Statsig
|
|
121
120
|
|
122
121
|
def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
123
122
|
delegate = eval_result.config_delegate
|
124
|
-
result[:explicit_parameters] = config_spec
|
123
|
+
result[:explicit_parameters] = config_spec[:explicitParameters] || []
|
125
124
|
|
126
125
|
if delegate.nil? == false && delegate.empty? == false
|
127
|
-
delegate_spec = evaluator.spec_store.configs[delegate]
|
126
|
+
delegate_spec = evaluator.spec_store.configs[delegate.to_sym]
|
128
127
|
|
129
128
|
result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
|
130
129
|
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
131
|
-
result[:is_experiment_active] = delegate_spec
|
132
|
-
result[:explicit_parameters] = delegate_spec
|
130
|
+
result[:is_experiment_active] = delegate_spec[:isActive] == true
|
131
|
+
result[:explicit_parameters] = delegate_spec[:explicitParameters] || []
|
133
132
|
end
|
134
133
|
|
135
134
|
if include_exposures
|
data/lib/constants.rb
CHANGED
@@ -47,6 +47,12 @@ module Statsig
|
|
47
47
|
USER_ID = 'user_id'.freeze
|
48
48
|
USERAGENT = 'useragent'.freeze
|
49
49
|
USERID = 'userid'.freeze
|
50
|
+
DICTIONARY = 'dictionary'.freeze
|
51
|
+
SEGMENT_PREFIX = 'segment:'.freeze
|
52
|
+
|
53
|
+
DYNAMIC_CONFIG_NAME = 'dynamic_config_name'.freeze
|
54
|
+
EXPERIMENT_NAME = 'experiment_name'.freeze
|
55
|
+
LAYER_NAME = 'layer_name'.freeze
|
50
56
|
|
51
57
|
# Persisted Evaluations
|
52
58
|
GATE_VALUE = 'gate_value'.freeze
|
@@ -58,5 +64,64 @@ module Statsig
|
|
58
64
|
TARGET_APP_IDS = 'target_app_ids'.freeze
|
59
65
|
CONFIG_SYNC_TIME = 'config_sync_time'.freeze
|
60
66
|
INIT_TIME = 'init_time'.freeze
|
67
|
+
|
68
|
+
# Spec Types
|
69
|
+
TYPE_FEATURE_GATE = 'feature_gate'.freeze
|
70
|
+
TYPE_SEGMENT = 'segment'.freeze
|
71
|
+
TYPE_HOLDOUT = 'holdout'.freeze
|
72
|
+
TYPE_EXPERIMENT = 'experiment'.freeze
|
73
|
+
TYPE_LAYER = 'layer'.freeze
|
74
|
+
TYPE_DYNAMIC_CONFIG = 'dynamic_config'.freeze
|
75
|
+
TYPE_AUTOTUNE = 'autotune'.freeze
|
76
|
+
|
77
|
+
# API Conditions
|
78
|
+
CND_PUBLIC = 'public'.freeze
|
79
|
+
CND_IP_BASED = 'ip_based'.freeze
|
80
|
+
CND_UA_BASED = 'ua_based'.freeze
|
81
|
+
CND_USER_FIELD = 'user_field'.freeze
|
82
|
+
CND_PASS_GATE = 'pass_gate'.freeze
|
83
|
+
CND_FAIL_GATE = 'fail_gate'.freeze
|
84
|
+
CND_MULTI_PASS_GATE = 'multi_pass_gate'.freeze
|
85
|
+
CND_MULTI_FAIL_GATE = 'multi_fail_gate'.freeze
|
86
|
+
CND_CURRENT_TIME = 'current_time'.freeze
|
87
|
+
CND_ENVIRONMENT_FIELD = 'environment_field'.freeze
|
88
|
+
CND_USER_BUCKET = 'user_bucket'.freeze
|
89
|
+
CND_UNIT_ID = 'unit_id'.freeze
|
90
|
+
|
91
|
+
# API Operators
|
92
|
+
OP_GREATER_THAN = 'gt'.freeze
|
93
|
+
OP_GREATER_THAN_OR_EQUAL = 'gte'.freeze
|
94
|
+
OP_LESS_THAN = 'lt'.freeze
|
95
|
+
OP_LESS_THAN_OR_EQUAL = 'lte'.freeze
|
96
|
+
OP_ANY = 'any'.freeze
|
97
|
+
OP_NONE = 'none'.freeze
|
98
|
+
OP_ANY_CASE_SENSITIVE = 'any_case_sensitive'.freeze
|
99
|
+
OP_NONE_CASE_SENSITIVE = 'none_case_sensitive'.freeze
|
100
|
+
OP_EQUAL = 'eq'.freeze
|
101
|
+
OP_NOT_EQUAL = 'neq'.freeze
|
102
|
+
|
103
|
+
# API Operators (Version)
|
104
|
+
OP_VERSION_GREATER_THAN = 'version_gt'.freeze
|
105
|
+
OP_VERSION_GREATER_THAN_OR_EQUAL = 'version_gte'.freeze
|
106
|
+
OP_VERSION_LESS_THAN = 'version_lt'.freeze
|
107
|
+
OP_VERSION_LESS_THAN_OR_EQUAL = 'version_lte'.freeze
|
108
|
+
OP_VERSION_EQUAL = 'version_eq'.freeze
|
109
|
+
OP_VERSION_NOT_EQUAL = 'version_neq'.freeze
|
110
|
+
|
111
|
+
# API Operators (String)
|
112
|
+
OP_STR_STARTS_WITH_ANY = 'str_starts_with_any'.freeze
|
113
|
+
OP_STR_END_WITH_ANY = 'str_ends_with_any'.freeze
|
114
|
+
OP_STR_CONTAINS_ANY = 'str_contains_any'.freeze
|
115
|
+
OP_STR_CONTAINS_NONE = 'str_contains_none'.freeze
|
116
|
+
OP_STR_MATCHES = 'str_matches'.freeze
|
117
|
+
|
118
|
+
# API Operators (Time)
|
119
|
+
OP_BEFORE = 'before'.freeze
|
120
|
+
OP_AFTER = 'after'.freeze
|
121
|
+
OP_ON = 'on'.freeze
|
122
|
+
|
123
|
+
# API Operators (Segments)
|
124
|
+
OP_IN_SEGMENT_LIST = 'in_segment_list'.freeze
|
125
|
+
OP_NOT_IN_SEGMENT_LIST = 'not_in_segment_list'.freeze
|
61
126
|
end
|
62
127
|
end
|
data/lib/dynamic_config.rb
CHANGED
@@ -33,8 +33,12 @@ class DynamicConfig
|
|
33
33
|
# @param index The name of parameter being fetched
|
34
34
|
# @param default_value The fallback value if the name cannot be found
|
35
35
|
def get(index, default_value)
|
36
|
-
return default_value if @value.nil?
|
37
|
-
|
36
|
+
return default_value if @value.nil?
|
37
|
+
|
38
|
+
index_sym = index.to_sym
|
39
|
+
return default_value unless @value.key?(index_sym)
|
40
|
+
|
41
|
+
@value[index_sym]
|
38
42
|
end
|
39
43
|
|
40
44
|
##
|
@@ -44,8 +48,12 @@ class DynamicConfig
|
|
44
48
|
# @param index The name of parameter being fetched
|
45
49
|
# @param default_value The fallback value if the name cannot be found
|
46
50
|
def get_typed(index, default_value)
|
47
|
-
return default_value if @value.nil?
|
48
|
-
|
49
|
-
|
51
|
+
return default_value if @value.nil?
|
52
|
+
|
53
|
+
index_sym = index.to_sym
|
54
|
+
return default_value unless @value.key?(index_sym)
|
55
|
+
|
56
|
+
return default_value if @value[index_sym].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
|
57
|
+
@value[index_sym]
|
50
58
|
end
|
51
59
|
end
|
data/lib/evaluation_helpers.rb
CHANGED
@@ -25,15 +25,26 @@ module EvaluationHelpers
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def self.equal_string_in_array(array, value, ignore_case)
|
28
|
-
|
28
|
+
if array.is_a?(Hash)
|
29
|
+
return array.has_key?(value.to_sym)
|
30
|
+
end
|
29
31
|
|
30
32
|
str_value = value.to_s
|
31
33
|
str_value_downcased = nil
|
32
34
|
|
33
|
-
if
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
return false if array.nil?
|
36
|
+
|
37
|
+
return array.any? do |item|
|
38
|
+
next false if item.nil?
|
39
|
+
item_str = item.to_s
|
40
|
+
|
41
|
+
next false unless item_str.length == str_value.length
|
42
|
+
|
43
|
+
return true if item_str == str_value
|
44
|
+
next false unless ignore_case
|
45
|
+
|
46
|
+
str_value_downcased ||= str_value.downcase
|
47
|
+
item_str.downcase == str_value_downcased
|
37
48
|
end
|
38
49
|
end
|
39
50
|
|