statsig 1.34.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +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
|
@@ -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
|
|