statsig 1.30.0 → 1.32.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 +128 -0
- data/lib/client_initialize_helpers.rb +79 -108
- data/lib/config_result.rb +17 -32
- data/lib/constants.rb +60 -0
- data/lib/diagnostics.rb +21 -70
- data/lib/dynamic_config.rb +1 -24
- data/lib/error_boundary.rb +0 -5
- data/lib/evaluation_details.rb +5 -1
- data/lib/evaluation_helpers.rb +35 -3
- data/lib/evaluator.rb +332 -316
- data/lib/feature_gate.rb +0 -24
- data/lib/id_list.rb +1 -1
- data/lib/interfaces/data_store.rb +1 -1
- data/lib/interfaces/user_persistent_storage.rb +1 -1
- data/lib/layer.rb +1 -20
- data/lib/network.rb +6 -53
- data/lib/spec_store.rb +124 -115
- data/lib/statsig.rb +72 -97
- data/lib/statsig_driver.rb +73 -128
- data/lib/statsig_errors.rb +1 -1
- data/lib/statsig_event.rb +8 -8
- data/lib/statsig_logger.rb +29 -26
- data/lib/statsig_options.rb +0 -50
- data/lib/statsig_user.rb +27 -68
- data/lib/ua_parser.rb +1 -1
- data/lib/uri_helper.rb +1 -9
- data/lib/user_persistent_storage_utils.rb +0 -17
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd71ed81ad0f3658f5b6b480adba4a36f560357c67d6d9869b605e2d0e1e76c0
|
|
4
|
+
data.tar.gz: 59b7da08c4991da0e7b33dc08865612347442970932e71b8069bcd4518d50680
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3775510747a856d9ae300ccad20a74de3cbe000bc3bca59c535c64cbab1bcb46a83069aa917299ee6dd7032f44206eecd39f3602c92f18c39a8ebbb57b45d9e9
|
|
7
|
+
data.tar.gz: e0a03db37029a260614a6eb8bf9edf1e2fe60f84d67fb9e027bf5fd72c3c4f969a7ab3145f55d0bd19f796b7d1733416547688e1076c69e066abd819c99b899c
|
data/lib/api_config.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require 'constants'
|
|
2
|
+
|
|
3
|
+
class UnsupportedConfigException < StandardError
|
|
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 initialize(name:, type:, is_active:, salt:, default_value:, enabled:, rules:, id_type:, entity:,
|
|
12
|
+
explicit_parameters: nil, has_shared_params: nil, target_app_ids: nil)
|
|
13
|
+
@name = name
|
|
14
|
+
@type = type.to_sym unless entity.nil?
|
|
15
|
+
@is_active = is_active
|
|
16
|
+
@salt = salt
|
|
17
|
+
@default_value = JSON.parse(JSON.generate(default_value))
|
|
18
|
+
@enabled = enabled
|
|
19
|
+
@rules = rules
|
|
20
|
+
@id_type = id_type
|
|
21
|
+
@entity = entity.to_sym unless entity.nil?
|
|
22
|
+
@explicit_parameters = explicit_parameters
|
|
23
|
+
@has_shared_params = has_shared_params
|
|
24
|
+
@target_app_ids = target_app_ids
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_json(json)
|
|
28
|
+
new(
|
|
29
|
+
name: json[:name],
|
|
30
|
+
type: json[:type],
|
|
31
|
+
is_active: json[:isActive],
|
|
32
|
+
salt: json[:salt],
|
|
33
|
+
default_value: json[:defaultValue] || {},
|
|
34
|
+
enabled: json[:enabled],
|
|
35
|
+
rules: json[:rules]&.map do |rule|
|
|
36
|
+
APIRule.from_json(rule)
|
|
37
|
+
end,
|
|
38
|
+
id_type: json[:idType],
|
|
39
|
+
entity: json[:entity],
|
|
40
|
+
explicit_parameters: json[:explicitParameters],
|
|
41
|
+
has_shared_params: json[:hasSharedParams],
|
|
42
|
+
target_app_ids: json[:targetAppIDs]
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module Statsig
|
|
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 initialize(name:, pass_percentage:, return_value:, id:, salt:, conditions:, id_type:,
|
|
55
|
+
group_name: nil, config_delegate: nil, is_experiment_group: nil)
|
|
56
|
+
@name = name
|
|
57
|
+
@pass_percentage = pass_percentage.to_f
|
|
58
|
+
@return_value = JSON.parse(JSON.generate(return_value))
|
|
59
|
+
@id = id
|
|
60
|
+
@salt = salt
|
|
61
|
+
@conditions = conditions
|
|
62
|
+
@id_type = id_type
|
|
63
|
+
@group_name = group_name
|
|
64
|
+
@config_delegate = config_delegate
|
|
65
|
+
@is_experiment_group = is_experiment_group
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.from_json(json)
|
|
69
|
+
new(
|
|
70
|
+
name: json[:name],
|
|
71
|
+
pass_percentage: json[:passPercentage],
|
|
72
|
+
return_value: json[:returnValue] || {},
|
|
73
|
+
id: json[:id],
|
|
74
|
+
salt: json[:salt],
|
|
75
|
+
conditions: json[:conditions]&.map do |condition|
|
|
76
|
+
APICondition.from_json(condition)
|
|
77
|
+
end,
|
|
78
|
+
id_type: json[:idType],
|
|
79
|
+
group_name: json[:groupName],
|
|
80
|
+
config_delegate: json[:configDelegate],
|
|
81
|
+
is_experiment_group: json[:isExperimentGroup]
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
module Statsig
|
|
88
|
+
class APICondition
|
|
89
|
+
|
|
90
|
+
attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type
|
|
91
|
+
|
|
92
|
+
def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:)
|
|
93
|
+
@type = type.to_sym unless type.nil?
|
|
94
|
+
@target_value = target_value
|
|
95
|
+
@operator = operator.to_sym unless operator.nil?
|
|
96
|
+
@field = field
|
|
97
|
+
@additional_values = additional_values || {}
|
|
98
|
+
@id_type = id_type
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.from_json(json)
|
|
102
|
+
operator = json[:operator]
|
|
103
|
+
unless operator.nil?
|
|
104
|
+
operator = operator&.downcase&.to_sym
|
|
105
|
+
unless Const::SUPPORTED_OPERATORS.include?(operator)
|
|
106
|
+
raise UnsupportedConfigException
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
type = json[:type]
|
|
111
|
+
unless type.nil?
|
|
112
|
+
type = type&.downcase&.to_sym
|
|
113
|
+
unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
|
|
114
|
+
raise UnsupportedConfigException
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
new(
|
|
119
|
+
type: json[:type],
|
|
120
|
+
target_value: json[:targetValue],
|
|
121
|
+
operator: json[:operator],
|
|
122
|
+
field: json[:field],
|
|
123
|
+
additional_values: json[:additionalValues],
|
|
124
|
+
id_type: json[:idType]
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -1,167 +1,138 @@
|
|
|
1
|
-
# typed: true
|
|
2
|
-
|
|
3
1
|
require_relative 'hash_utils'
|
|
4
|
-
require 'sorbet-runtime'
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
:gate_value => false,
|
|
8
|
-
:json_value => {},
|
|
9
|
-
:rule_id => "",
|
|
10
|
-
:is_experiment_group => false,
|
|
11
|
-
:secondary_exposures => []
|
|
12
|
-
}
|
|
3
|
+
require 'constants'
|
|
13
4
|
|
|
14
|
-
module
|
|
5
|
+
module Statsig
|
|
15
6
|
class ResponseFormatter
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@client_sdk_key = client_sdk_key
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def get_responses(key)
|
|
27
|
-
@specs[key]
|
|
28
|
-
.map { |name, spec| to_response(name, spec) }
|
|
29
|
-
.delete_if { |v| v.nil? }.to_h
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
sig { params(secondary_exposures: T::Array[T::Hash[String, String]]).returns(T::Array[T::Hash[String, String]]) }
|
|
35
|
-
def filter_segments_from_secondary_exposures(secondary_exposures)
|
|
36
|
-
secondary_exposures.reject do |exposure|
|
|
37
|
-
exposure['gate'].to_s.start_with?('segment:')
|
|
7
|
+
def self.get_responses(entities, evaluator, user, client_sdk_key, hash_algo, include_exposures: true)
|
|
8
|
+
result = {}
|
|
9
|
+
entities.each do |name, spec|
|
|
10
|
+
hashed_name, value = to_response(name, spec, evaluator, user, client_sdk_key, hash_algo, include_exposures)
|
|
11
|
+
if !hashed_name.nil? && !value.nil?
|
|
12
|
+
result[hashed_name] = value
|
|
13
|
+
end
|
|
38
14
|
end
|
|
15
|
+
|
|
16
|
+
result
|
|
39
17
|
end
|
|
40
18
|
|
|
41
|
-
def to_response(config_name, config_spec)
|
|
42
|
-
target_app_id =
|
|
43
|
-
config_target_apps = config_spec
|
|
19
|
+
def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures)
|
|
20
|
+
target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
|
|
21
|
+
config_target_apps = config_spec.target_app_ids
|
|
44
22
|
|
|
45
23
|
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
|
46
24
|
return nil
|
|
47
25
|
end
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
category = config_spec.type
|
|
28
|
+
entity_type = config_spec.entity
|
|
29
|
+
if entity_type == :segment || entity_type == :holdout
|
|
51
30
|
return nil
|
|
52
31
|
end
|
|
53
32
|
|
|
54
|
-
|
|
55
|
-
:
|
|
56
|
-
:
|
|
57
|
-
:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
:config_delegate => eval_result.config_delegate,
|
|
61
|
-
:is_experiment_group => eval_result.is_experiment_group,
|
|
62
|
-
:secondary_exposures => filter_segments_from_secondary_exposures(eval_result.secondary_exposures),
|
|
63
|
-
:undelegated_sec_exps => eval_result.undelegated_sec_exps
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
category = config_spec['type']
|
|
67
|
-
entity_type = config_spec['entity']
|
|
33
|
+
eval_result = ConfigResult.new(
|
|
34
|
+
name: config_name,
|
|
35
|
+
disable_evaluation_details: true,
|
|
36
|
+
disable_exposures: !include_exposures
|
|
37
|
+
)
|
|
38
|
+
evaluator.eval_spec(user, config_spec, eval_result)
|
|
68
39
|
|
|
69
40
|
result = {}
|
|
70
41
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return nil
|
|
76
|
-
end
|
|
42
|
+
result[:id_type] = eval_result.id_type
|
|
43
|
+
unless eval_result.group_name.nil?
|
|
44
|
+
result[:group_name] = eval_result.group_name
|
|
45
|
+
end
|
|
77
46
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
result[
|
|
81
|
-
when
|
|
82
|
-
id_type = config_spec
|
|
83
|
-
result[
|
|
84
|
-
result[
|
|
85
|
-
result[
|
|
86
|
-
result["id_type"] = safe_eval_result[:id_type]
|
|
87
|
-
result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
|
|
47
|
+
case category
|
|
48
|
+
when :feature_gate
|
|
49
|
+
result[:value] = eval_result.gate_value
|
|
50
|
+
when :dynamic_config
|
|
51
|
+
id_type = config_spec.id_type
|
|
52
|
+
result[:value] = eval_result.json_value
|
|
53
|
+
result[:group] = eval_result.rule_id
|
|
54
|
+
result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
|
|
88
55
|
else
|
|
89
56
|
return nil
|
|
90
57
|
end
|
|
91
58
|
|
|
92
|
-
if entity_type ==
|
|
93
|
-
populate_experiment_fields(
|
|
59
|
+
if entity_type == :experiment
|
|
60
|
+
populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
|
|
94
61
|
end
|
|
95
62
|
|
|
96
|
-
if entity_type ==
|
|
97
|
-
populate_layer_fields(config_spec,
|
|
98
|
-
result.delete(
|
|
63
|
+
if entity_type == :layer
|
|
64
|
+
populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
|
65
|
+
result.delete(:id_type) # not exposed for layer configs in /initialize
|
|
99
66
|
end
|
|
100
67
|
|
|
101
|
-
hashed_name = hash_name(config_name)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
68
|
+
hashed_name = hash_name(config_name, hash_algo)
|
|
69
|
+
|
|
70
|
+
result[:name] = hashed_name
|
|
71
|
+
result[:rule_id] = eval_result.rule_id
|
|
72
|
+
|
|
73
|
+
if include_exposures
|
|
74
|
+
result[:secondary_exposures] = clean_exposures(eval_result.secondary_exposures)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
[hashed_name, result]
|
|
108
78
|
end
|
|
109
79
|
|
|
110
|
-
def clean_exposures(exposures)
|
|
80
|
+
def self.clean_exposures(exposures)
|
|
111
81
|
seen = {}
|
|
112
82
|
exposures.reject do |exposure|
|
|
113
|
-
key = "#{exposure[
|
|
83
|
+
key = "#{exposure[:gate]}|#{exposure[:gateValue]}|#{exposure[:ruleID]}}"
|
|
114
84
|
should_reject = seen[key]
|
|
115
85
|
seen[key] = true
|
|
116
86
|
should_reject == true
|
|
117
87
|
end
|
|
118
88
|
end
|
|
119
89
|
|
|
120
|
-
def populate_experiment_fields(config_name, config_spec, eval_result, result)
|
|
121
|
-
result[
|
|
122
|
-
result[
|
|
90
|
+
def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
|
|
91
|
+
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
|
92
|
+
result[:is_experiment_active] = config_spec.is_active == true
|
|
123
93
|
|
|
124
|
-
if config_spec
|
|
94
|
+
if config_spec.has_shared_params != true
|
|
125
95
|
return
|
|
126
96
|
end
|
|
127
97
|
|
|
128
|
-
result[
|
|
129
|
-
result[
|
|
98
|
+
result[:is_in_layer] = true
|
|
99
|
+
result[:explicit_parameters] = config_spec.explicit_parameters || []
|
|
130
100
|
|
|
131
|
-
layer_name =
|
|
132
|
-
if layer_name.nil? ||
|
|
101
|
+
layer_name = evaluator.spec_store.experiment_to_layer[config_name]
|
|
102
|
+
if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
|
|
133
103
|
return
|
|
134
104
|
end
|
|
135
105
|
|
|
136
|
-
layer =
|
|
137
|
-
result[
|
|
106
|
+
layer = evaluator.spec_store.layers[layer_name]
|
|
107
|
+
result[:value] = layer[:defaultValue].merge(result[:value])
|
|
138
108
|
end
|
|
139
109
|
|
|
140
|
-
def populate_layer_fields(config_spec, eval_result, result)
|
|
141
|
-
delegate = eval_result
|
|
142
|
-
result[
|
|
110
|
+
def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
|
111
|
+
delegate = eval_result.config_delegate
|
|
112
|
+
result[:explicit_parameters] = config_spec.explicit_parameters || []
|
|
143
113
|
|
|
144
114
|
if delegate.nil? == false && delegate.empty? == false
|
|
145
|
-
delegate_spec =
|
|
146
|
-
delegate_result = @evaluator.eval_spec(@user, delegate_spec)
|
|
115
|
+
delegate_spec = evaluator.spec_store.configs[delegate]
|
|
147
116
|
|
|
148
|
-
result[
|
|
149
|
-
result[
|
|
150
|
-
result[
|
|
151
|
-
result[
|
|
117
|
+
result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
|
|
118
|
+
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
|
119
|
+
result[:is_experiment_active] = delegate_spec.is_active == true
|
|
120
|
+
result[:explicit_parameters] = delegate_spec.explicit_parameters || []
|
|
152
121
|
end
|
|
153
122
|
|
|
154
|
-
|
|
123
|
+
if include_exposures
|
|
124
|
+
result[:undelegated_secondary_exposures] = clean_exposures(eval_result.undelegated_sec_exps || [])
|
|
125
|
+
end
|
|
155
126
|
end
|
|
156
127
|
|
|
157
|
-
def hash_name(name)
|
|
158
|
-
case
|
|
159
|
-
when
|
|
128
|
+
def self.hash_name(name, hash_algo)
|
|
129
|
+
case hash_algo
|
|
130
|
+
when Statsig::Const::NONE
|
|
160
131
|
return name
|
|
161
|
-
when
|
|
162
|
-
return Statsig::HashUtils.sha256(name)
|
|
163
|
-
when 'djb2'
|
|
132
|
+
when Statsig::Const::DJB2
|
|
164
133
|
return Statsig::HashUtils.djb2(name)
|
|
134
|
+
else
|
|
135
|
+
return Statsig::HashUtils.sha256(name)
|
|
165
136
|
end
|
|
166
137
|
end
|
|
167
138
|
end
|
data/lib/config_result.rb
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
# typed: true
|
|
2
|
-
|
|
3
|
-
require 'sorbet-runtime'
|
|
4
|
-
|
|
5
1
|
module Statsig
|
|
6
2
|
class ConfigResult
|
|
7
|
-
extend T::Sig
|
|
8
3
|
|
|
9
4
|
attr_accessor :name
|
|
10
5
|
attr_accessor :gate_value
|
|
@@ -19,25 +14,30 @@ module Statsig
|
|
|
19
14
|
attr_accessor :group_name
|
|
20
15
|
attr_accessor :id_type
|
|
21
16
|
attr_accessor :target_app_ids
|
|
17
|
+
attr_accessor :disable_evaluation_details
|
|
18
|
+
attr_accessor :disable_exposures
|
|
22
19
|
|
|
23
20
|
def initialize(
|
|
24
|
-
name
|
|
25
|
-
gate_value
|
|
26
|
-
json_value
|
|
27
|
-
rule_id
|
|
28
|
-
secondary_exposures
|
|
29
|
-
config_delegate
|
|
30
|
-
explicit_parameters
|
|
21
|
+
name:,
|
|
22
|
+
gate_value: false,
|
|
23
|
+
json_value: nil,
|
|
24
|
+
rule_id: nil,
|
|
25
|
+
secondary_exposures: [],
|
|
26
|
+
config_delegate: nil,
|
|
27
|
+
explicit_parameters: nil,
|
|
31
28
|
is_experiment_group: false,
|
|
32
29
|
evaluation_details: nil,
|
|
33
30
|
group_name: nil,
|
|
34
|
-
id_type:
|
|
35
|
-
target_app_ids: nil
|
|
31
|
+
id_type: nil,
|
|
32
|
+
target_app_ids: nil,
|
|
33
|
+
disable_evaluation_details: false,
|
|
34
|
+
disable_exposures: false
|
|
35
|
+
)
|
|
36
36
|
@name = name
|
|
37
37
|
@gate_value = gate_value
|
|
38
38
|
@json_value = json_value
|
|
39
39
|
@rule_id = rule_id
|
|
40
|
-
@secondary_exposures = secondary_exposures
|
|
40
|
+
@secondary_exposures = secondary_exposures
|
|
41
41
|
@undelegated_sec_exps = @secondary_exposures
|
|
42
42
|
@config_delegate = config_delegate
|
|
43
43
|
@explicit_parameters = explicit_parameters
|
|
@@ -46,9 +46,10 @@ module Statsig
|
|
|
46
46
|
@group_name = group_name
|
|
47
47
|
@id_type = id_type
|
|
48
48
|
@target_app_ids = target_app_ids
|
|
49
|
+
@disable_evaluation_details = disable_evaluation_details
|
|
50
|
+
@disable_exposures = disable_exposures
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
|
|
52
53
|
def self.from_user_persisted_values(config_name, user_persisted_values)
|
|
53
54
|
sticky_values = user_persisted_values[config_name]
|
|
54
55
|
return nil if sticky_values.nil?
|
|
@@ -56,22 +57,6 @@ module Statsig
|
|
|
56
57
|
from_hash(config_name, sticky_values)
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
sig { params(config_name: String, hash: Hash).returns(ConfigResult) }
|
|
60
|
-
def self.from_hash(config_name, hash)
|
|
61
|
-
new(
|
|
62
|
-
config_name,
|
|
63
|
-
hash['gate_value'],
|
|
64
|
-
hash['json_value'],
|
|
65
|
-
hash['rule_id'],
|
|
66
|
-
hash['secondary_exposures'],
|
|
67
|
-
evaluation_details: EvaluationDetails.persisted(hash['config_sync_time'], hash['init_time']),
|
|
68
|
-
group_name: hash['group_name'],
|
|
69
|
-
id_type: hash['id_type'],
|
|
70
|
-
target_app_ids: hash['target_app_ids']
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
sig { returns(Hash) }
|
|
75
60
|
def to_hash
|
|
76
61
|
{
|
|
77
62
|
json_value: @json_value,
|
data/lib/constants.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Statsig
|
|
2
|
+
module Const
|
|
3
|
+
EMPTY_STR = ''.freeze
|
|
4
|
+
|
|
5
|
+
SUPPORTED_CONDITION_TYPES = Set.new(%i[
|
|
6
|
+
public fail_gate pass_gate ip_based ua_based user_field
|
|
7
|
+
environment_field current_time user_bucket unit_id
|
|
8
|
+
]).freeze
|
|
9
|
+
|
|
10
|
+
SUPPORTED_OPERATORS = Set.new(%i[
|
|
11
|
+
gt gte lt lte version_gt version_gte version_lt version_lte
|
|
12
|
+
version_eq version_neq any none any_case_sensitive none_case_sensitive
|
|
13
|
+
str_starts_with_any str_ends_with_any str_contains_any str_contains_none
|
|
14
|
+
str_matches eq neq before after on in_segment_list not_in_segment_list
|
|
15
|
+
]).freeze
|
|
16
|
+
|
|
17
|
+
APP_VERSION = 'app_version'.freeze
|
|
18
|
+
APPVERSION = 'appversion'.freeze
|
|
19
|
+
BROWSER_NAME = 'browser_name'.freeze
|
|
20
|
+
BROWSER_VERSION = 'browser_version'.freeze
|
|
21
|
+
BROWSERNAME = 'browsername'.freeze
|
|
22
|
+
BROWSERVERSION = 'browserversion'.freeze
|
|
23
|
+
CML_SHA_256 = 'sha256'.freeze
|
|
24
|
+
CML_USER_ID = 'userID'.freeze
|
|
25
|
+
COUNTRY = 'country'.freeze
|
|
26
|
+
DEFAULT = 'default'.freeze
|
|
27
|
+
DISABLED = 'disabled'.freeze
|
|
28
|
+
DJB2 = 'djb2'.freeze
|
|
29
|
+
EMAIL = 'email'.freeze
|
|
30
|
+
FALSE = 'false'.freeze
|
|
31
|
+
IP = 'ip'.freeze
|
|
32
|
+
LAYER = :layer
|
|
33
|
+
LOCALE = 'locale'.freeze
|
|
34
|
+
NONE = 'none'.freeze
|
|
35
|
+
OS_NAME = 'os_name'.freeze
|
|
36
|
+
OS_VERSION = 'os_version'.freeze
|
|
37
|
+
OSNAME = 'osname'.freeze
|
|
38
|
+
OSVERSION = 'osversion'.freeze
|
|
39
|
+
OVERRIDE = 'override'.freeze
|
|
40
|
+
Q_RIGHT_CHEVRON = 'Q>'.freeze
|
|
41
|
+
STABLEID = 'stableid'.freeze
|
|
42
|
+
STATSIG_RUBY_SDK = 'statsig-ruby-sdk'.freeze
|
|
43
|
+
TRUE = 'true'.freeze
|
|
44
|
+
USER_AGENT = 'user_agent'.freeze
|
|
45
|
+
USER_ID = 'user_id'.freeze
|
|
46
|
+
USERAGENT = 'useragent'.freeze
|
|
47
|
+
USERID = 'userid'.freeze
|
|
48
|
+
|
|
49
|
+
# Persisted Evaluations
|
|
50
|
+
GATE_VALUE = 'gate_value'.freeze
|
|
51
|
+
JSON_VALUE = 'json_value'.freeze
|
|
52
|
+
RULE_ID = 'rule_id'.freeze
|
|
53
|
+
SECONDARY_EXPOSURES = 'secondary_exposures'.freeze
|
|
54
|
+
GROUP_NAME = 'group_name'.freeze
|
|
55
|
+
ID_TYPE = 'id_type'.freeze
|
|
56
|
+
TARGET_APP_IDS = 'target_app_ids'.freeze
|
|
57
|
+
CONFIG_SYNC_TIME = 'config_sync_time'.freeze
|
|
58
|
+
INIT_TIME = 'init_time'.freeze
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/diagnostics.rb
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
# typed: true
|
|
2
|
-
|
|
3
|
-
require 'sorbet-runtime'
|
|
4
|
-
|
|
5
1
|
module Statsig
|
|
6
2
|
class Diagnostics
|
|
7
|
-
extend T::Sig
|
|
8
|
-
|
|
9
|
-
sig { returns(String) }
|
|
10
|
-
attr_accessor :context
|
|
11
|
-
|
|
12
|
-
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
13
3
|
attr_reader :markers
|
|
14
4
|
|
|
15
|
-
sig { returns(T::Hash[String, Numeric]) }
|
|
16
5
|
attr_accessor :sample_rates
|
|
17
6
|
|
|
18
|
-
def initialize(
|
|
19
|
-
@
|
|
20
|
-
@markers = []
|
|
7
|
+
def initialize()
|
|
8
|
+
@markers = {:initialize => [], :api_call => [], :config_sync => []}
|
|
21
9
|
@sample_rates = {}
|
|
22
10
|
end
|
|
23
11
|
|
|
24
|
-
|
|
25
|
-
params(
|
|
26
|
-
key: String,
|
|
27
|
-
action: String,
|
|
28
|
-
step: T.any(String, NilClass),
|
|
29
|
-
tags: T::Hash[Symbol, T.untyped]
|
|
30
|
-
).void
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def mark(key, action, step, tags)
|
|
12
|
+
def mark(key, action, step, tags, context)
|
|
34
13
|
marker = {
|
|
35
14
|
key: key,
|
|
36
15
|
action: action,
|
|
@@ -44,87 +23,59 @@ module Statsig
|
|
|
44
23
|
marker[key] = val
|
|
45
24
|
end
|
|
46
25
|
end
|
|
47
|
-
@markers.
|
|
26
|
+
if @markers[context].nil?
|
|
27
|
+
@markers[context] = []
|
|
28
|
+
end
|
|
29
|
+
@markers[context].push(marker)
|
|
48
30
|
end
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
key: String,
|
|
53
|
-
step: T.any(String, NilClass),
|
|
54
|
-
tags: T::Hash[Symbol, T.untyped]
|
|
55
|
-
).returns(Tracker)
|
|
56
|
-
end
|
|
57
|
-
def track(key, step = nil, tags = {})
|
|
58
|
-
tracker = Tracker.new(self, key, step, tags)
|
|
32
|
+
def track(context, key, step = nil, tags = {})
|
|
33
|
+
tracker = Tracker.new(self, context, key, step, tags)
|
|
59
34
|
tracker.start(**tags)
|
|
60
35
|
tracker
|
|
61
36
|
end
|
|
62
37
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def serialize
|
|
66
|
-
{
|
|
67
|
-
context: @context.clone,
|
|
68
|
-
markers: @markers.clone
|
|
69
|
-
}
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def serialize_with_sampling
|
|
73
|
-
marker_keys = @markers.map { |e| e[:key] }
|
|
38
|
+
def serialize_with_sampling(context)
|
|
39
|
+
marker_keys = @markers[context].map { |e| e[:key] }
|
|
74
40
|
unique_marker_keys = marker_keys.uniq { |e| e }
|
|
75
41
|
sampled_marker_keys = unique_marker_keys.select do |key|
|
|
76
42
|
@sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
|
|
77
43
|
end
|
|
78
|
-
final_markers = @markers.select do |marker|
|
|
44
|
+
final_markers = @markers[context].select do |marker|
|
|
79
45
|
!sampled_marker_keys.include?(marker[:key])
|
|
80
46
|
end
|
|
81
47
|
{
|
|
82
|
-
context:
|
|
83
|
-
markers: final_markers
|
|
48
|
+
context: context.clone,
|
|
49
|
+
markers: final_markers.clone
|
|
84
50
|
}
|
|
85
51
|
end
|
|
86
52
|
|
|
87
|
-
def clear_markers
|
|
88
|
-
@markers.clear
|
|
53
|
+
def clear_markers(context)
|
|
54
|
+
@markers[context].clear
|
|
89
55
|
end
|
|
90
56
|
|
|
91
57
|
def self.sample(rate_over_ten_thousand)
|
|
92
58
|
rand * 10_000 < rate_over_ten_thousand
|
|
93
59
|
end
|
|
94
60
|
|
|
95
|
-
class Context
|
|
96
|
-
INITIALIZE = 'initialize'.freeze
|
|
97
|
-
CONFIG_SYNC = 'config_sync'.freeze
|
|
98
|
-
API_CALL = 'api_call'.freeze
|
|
99
|
-
end
|
|
100
|
-
|
|
101
61
|
API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
|
|
102
62
|
|
|
103
63
|
class Tracker
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
sig do
|
|
107
|
-
params(
|
|
108
|
-
diagnostics: Diagnostics,
|
|
109
|
-
key: String,
|
|
110
|
-
step: T.any(String, NilClass),
|
|
111
|
-
tags: T::Hash[Symbol, T.untyped]
|
|
112
|
-
).void
|
|
113
|
-
end
|
|
114
|
-
def initialize(diagnostics, key, step, tags = {})
|
|
64
|
+
def initialize(diagnostics, context, key, step, tags = {})
|
|
115
65
|
@diagnostics = diagnostics
|
|
66
|
+
@context = context
|
|
116
67
|
@key = key
|
|
117
68
|
@step = step
|
|
118
69
|
@tags = tags
|
|
119
70
|
end
|
|
120
71
|
|
|
121
72
|
def start(**tags)
|
|
122
|
-
@diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
|
|
73
|
+
@diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags), @context)
|
|
123
74
|
end
|
|
124
75
|
|
|
125
76
|
def end(**tags)
|
|
126
|
-
@diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
|
|
77
|
+
@diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags), @context)
|
|
127
78
|
end
|
|
128
79
|
end
|
|
129
80
|
end
|
|
130
|
-
end
|
|
81
|
+
end
|