statsig 1.34.2 → 2.0.1
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 +21 -20
- data/lib/constants.rb +65 -0
- data/lib/dynamic_config.rb +13 -5
- data/lib/evaluation_helpers.rb +16 -5
- data/lib/evaluator.rb +147 -117
- 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: '059ef73434d227fde96790307f75b6922f1dbd840f3e7afca68c4600f217263d'
|
4
|
+
data.tar.gz: 7eb241bdd104280ee49117094859ba9b9801747c32a2b4e8ed043b96e336a812
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0b92e135f883955bd187971886822959656c644a9573f96de043c342f2930787152c0c16b293d8ed33cda25559dcd1298f9578056fc049a2042752a168ef0b1
|
7
|
+
data.tar.gz: c0103fd2da0b70705540e391aa6afc2bfcd81a37a1dfa01791032aeb43b258ff782caf6eba8c8e4188b15d8676a643d8764a2bc9b6f2e9a022b07455fec221c3
|
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
|
@@ -16,7 +16,7 @@ module Statsig
|
|
16
16
|
result = {}
|
17
17
|
target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
|
18
18
|
entities.each do |name, spec|
|
19
|
-
config_target_apps = spec
|
19
|
+
config_target_apps = spec[:targetAppIDs]
|
20
20
|
|
21
21
|
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
22
22
|
next
|
@@ -31,17 +31,18 @@ module Statsig
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
37
38
|
return nil
|
38
39
|
end
|
39
40
|
|
40
41
|
if include_local_overrides
|
41
42
|
case category
|
42
|
-
when
|
43
|
+
when Const::TYPE_FEATURE_GATE
|
43
44
|
local_override = evaluator.lookup_gate_override(config_name)
|
44
|
-
when
|
45
|
+
when Const::TYPE_DYNAMIC_CONFIG
|
45
46
|
local_override = evaluator.lookup_config_override(config_name)
|
46
47
|
end
|
47
48
|
end
|
@@ -52,7 +53,7 @@ module Statsig
|
|
52
53
|
disable_evaluation_details: true,
|
53
54
|
disable_exposures: !include_exposures
|
54
55
|
)
|
55
|
-
evaluator.eval_spec(user, config_spec, eval_result)
|
56
|
+
evaluator.eval_spec(config_name_str, user, config_spec, eval_result)
|
56
57
|
else
|
57
58
|
eval_result = local_override
|
58
59
|
end
|
@@ -65,10 +66,10 @@ module Statsig
|
|
65
66
|
end
|
66
67
|
|
67
68
|
case category
|
68
|
-
when
|
69
|
+
when Const::TYPE_FEATURE_GATE
|
69
70
|
result[:value] = eval_result.gate_value
|
70
|
-
when
|
71
|
-
id_type = config_spec
|
71
|
+
when Const::TYPE_DYNAMIC_CONFIG
|
72
|
+
id_type = config_spec[:idType]
|
72
73
|
result[:value] = eval_result.json_value
|
73
74
|
result[:group] = eval_result.rule_id
|
74
75
|
result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
|
@@ -76,16 +77,16 @@ module Statsig
|
|
76
77
|
return nil
|
77
78
|
end
|
78
79
|
|
79
|
-
if entity_type ==
|
80
|
+
if entity_type == Const::TYPE_EXPERIMENT
|
80
81
|
populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
|
81
82
|
end
|
82
83
|
|
83
|
-
if entity_type ==
|
84
|
+
if entity_type == Const::TYPE_LAYER
|
84
85
|
populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
85
86
|
result.delete(:id_type) # not exposed for layer configs in /initialize
|
86
87
|
end
|
87
88
|
|
88
|
-
hashed_name = hash_name(
|
89
|
+
hashed_name = hash_name(config_name_str, hash_algo)
|
89
90
|
|
90
91
|
result[:name] = hashed_name
|
91
92
|
result[:rule_id] = eval_result.rule_id
|
@@ -99,14 +100,14 @@ module Statsig
|
|
99
100
|
|
100
101
|
def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
|
101
102
|
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
102
|
-
result[:is_experiment_active] = config_spec
|
103
|
+
result[:is_experiment_active] = config_spec[:isActive] == true
|
103
104
|
|
104
|
-
if config_spec
|
105
|
+
if config_spec[:hasSharedParams] != true
|
105
106
|
return
|
106
107
|
end
|
107
108
|
|
108
109
|
result[:is_in_layer] = true
|
109
|
-
result[:explicit_parameters] = config_spec
|
110
|
+
result[:explicit_parameters] = config_spec[:explicitParameters] || []
|
110
111
|
|
111
112
|
layer_name = evaluator.spec_store.experiment_to_layer[config_name]
|
112
113
|
if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
|
@@ -119,15 +120,15 @@ module Statsig
|
|
119
120
|
|
120
121
|
def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
121
122
|
delegate = eval_result.config_delegate
|
122
|
-
result[:explicit_parameters] = config_spec
|
123
|
+
result[:explicit_parameters] = config_spec[:explicitParameters] || []
|
123
124
|
|
124
125
|
if delegate.nil? == false && delegate.empty? == false
|
125
|
-
delegate_spec = evaluator.spec_store.configs[delegate]
|
126
|
+
delegate_spec = evaluator.spec_store.configs[delegate.to_sym]
|
126
127
|
|
127
128
|
result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
|
128
129
|
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
129
|
-
result[:is_experiment_active] = delegate_spec
|
130
|
-
result[:explicit_parameters] = delegate_spec
|
130
|
+
result[:is_experiment_active] = delegate_spec[:isActive] == true
|
131
|
+
result[:explicit_parameters] = delegate_spec[:explicitParameters] || []
|
131
132
|
end
|
132
133
|
|
133
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
|
|