statsig 1.31.1 → 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 +78 -88
- data/lib/config_result.rb +17 -32
- data/lib/constants.rb +60 -0
- data/lib/diagnostics.rb +1 -38
- data/lib/dynamic_config.rb +1 -24
- data/lib/error_boundary.rb +0 -5
- data/lib/evaluation_details.rb +4 -1
- data/lib/evaluation_helpers.rb +35 -3
- data/lib/evaluator.rb +332 -327
- 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 -33
- data/lib/spec_store.rb +86 -74
- data/lib/statsig.rb +72 -97
- data/lib/statsig_driver.rb +63 -70
- data/lib/statsig_errors.rb +1 -1
- data/lib/statsig_event.rb +8 -8
- data/lib/statsig_logger.rb +21 -21
- 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 +8 -6
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,148 +1,138 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
1
|
require_relative 'hash_utils'
|
4
|
-
require 'sorbet-runtime'
|
5
2
|
|
3
|
+
require 'constants'
|
6
4
|
|
7
|
-
module
|
5
|
+
module Statsig
|
8
6
|
class ResponseFormatter
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@client_sdk_key = client_sdk_key
|
17
|
-
end
|
18
|
-
|
19
|
-
def get_responses(key)
|
20
|
-
@specs[key]
|
21
|
-
.map { |name, spec| to_response(name, spec) }
|
22
|
-
.delete_if { |v| v.nil? }.to_h
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
sig { params(secondary_exposures: T::Array[T::Hash[String, String]]).returns(T::Array[T::Hash[String, String]]) }
|
28
|
-
def filter_segments_from_secondary_exposures(secondary_exposures)
|
29
|
-
secondary_exposures.reject do |exposure|
|
30
|
-
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
|
31
14
|
end
|
15
|
+
|
16
|
+
result
|
32
17
|
end
|
33
18
|
|
34
|
-
def to_response(config_name, config_spec)
|
35
|
-
target_app_id =
|
36
|
-
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
|
37
22
|
|
38
23
|
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
39
24
|
return nil
|
40
25
|
end
|
41
26
|
|
42
|
-
|
43
|
-
|
27
|
+
category = config_spec.type
|
28
|
+
entity_type = config_spec.entity
|
29
|
+
if entity_type == :segment || entity_type == :holdout
|
44
30
|
return nil
|
45
31
|
end
|
46
32
|
|
47
|
-
|
48
|
-
|
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)
|
49
39
|
|
50
40
|
result = {}
|
51
41
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
return nil
|
57
|
-
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
|
58
46
|
|
59
|
-
|
60
|
-
|
61
|
-
result[
|
62
|
-
when
|
63
|
-
id_type = config_spec
|
64
|
-
result[
|
65
|
-
result[
|
66
|
-
result[
|
67
|
-
result["id_type"] = eval_result.id_type
|
68
|
-
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
|
69
55
|
else
|
70
56
|
return nil
|
71
57
|
end
|
72
58
|
|
73
|
-
if entity_type ==
|
74
|
-
populate_experiment_fields(
|
59
|
+
if entity_type == :experiment
|
60
|
+
populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
|
75
61
|
end
|
76
62
|
|
77
|
-
if entity_type ==
|
78
|
-
populate_layer_fields(config_spec, eval_result, result)
|
79
|
-
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
|
80
66
|
end
|
81
67
|
|
82
|
-
hashed_name = hash_name(config_name)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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]
|
89
78
|
end
|
90
79
|
|
91
|
-
def clean_exposures(exposures)
|
80
|
+
def self.clean_exposures(exposures)
|
92
81
|
seen = {}
|
93
82
|
exposures.reject do |exposure|
|
94
|
-
key = "#{exposure[
|
83
|
+
key = "#{exposure[:gate]}|#{exposure[:gateValue]}|#{exposure[:ruleID]}}"
|
95
84
|
should_reject = seen[key]
|
96
85
|
seen[key] = true
|
97
86
|
should_reject == true
|
98
87
|
end
|
99
88
|
end
|
100
89
|
|
101
|
-
def populate_experiment_fields(config_name, config_spec, eval_result, result)
|
102
|
-
result[
|
103
|
-
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
|
104
93
|
|
105
|
-
if config_spec
|
94
|
+
if config_spec.has_shared_params != true
|
106
95
|
return
|
107
96
|
end
|
108
97
|
|
109
|
-
result[
|
110
|
-
result[
|
98
|
+
result[:is_in_layer] = true
|
99
|
+
result[:explicit_parameters] = config_spec.explicit_parameters || []
|
111
100
|
|
112
|
-
layer_name =
|
113
|
-
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?
|
114
103
|
return
|
115
104
|
end
|
116
105
|
|
117
|
-
layer =
|
118
|
-
result[
|
106
|
+
layer = evaluator.spec_store.layers[layer_name]
|
107
|
+
result[:value] = layer[:defaultValue].merge(result[:value])
|
119
108
|
end
|
120
109
|
|
121
|
-
def populate_layer_fields(config_spec, eval_result, result)
|
110
|
+
def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
122
111
|
delegate = eval_result.config_delegate
|
123
|
-
result[
|
112
|
+
result[:explicit_parameters] = config_spec.explicit_parameters || []
|
124
113
|
|
125
114
|
if delegate.nil? == false && delegate.empty? == false
|
126
|
-
delegate_spec =
|
127
|
-
delegate_result = @evaluator.eval_spec(@user, delegate_spec)
|
115
|
+
delegate_spec = evaluator.spec_store.configs[delegate]
|
128
116
|
|
129
|
-
result[
|
130
|
-
result[
|
131
|
-
result[
|
132
|
-
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 || []
|
133
121
|
end
|
134
122
|
|
135
|
-
|
123
|
+
if include_exposures
|
124
|
+
result[:undelegated_secondary_exposures] = clean_exposures(eval_result.undelegated_sec_exps || [])
|
125
|
+
end
|
136
126
|
end
|
137
127
|
|
138
|
-
def hash_name(name)
|
139
|
-
case
|
140
|
-
when
|
128
|
+
def self.hash_name(name, hash_algo)
|
129
|
+
case hash_algo
|
130
|
+
when Statsig::Const::NONE
|
141
131
|
return name
|
142
|
-
when
|
143
|
-
return Statsig::HashUtils.sha256(name)
|
144
|
-
when 'djb2'
|
132
|
+
when Statsig::Const::DJB2
|
145
133
|
return Statsig::HashUtils.djb2(name)
|
134
|
+
else
|
135
|
+
return Statsig::HashUtils.sha256(name)
|
146
136
|
end
|
147
137
|
end
|
148
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,15 +1,7 @@
|
|
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(T::Hash[String, T::Array[T::Hash[Symbol, T.untyped]]]) }
|
10
3
|
attr_reader :markers
|
11
4
|
|
12
|
-
sig { returns(T::Hash[String, Numeric]) }
|
13
5
|
attr_accessor :sample_rates
|
14
6
|
|
15
7
|
def initialize()
|
@@ -17,16 +9,6 @@ module Statsig
|
|
17
9
|
@sample_rates = {}
|
18
10
|
end
|
19
11
|
|
20
|
-
sig do
|
21
|
-
params(
|
22
|
-
key: String,
|
23
|
-
action: String,
|
24
|
-
step: T.any(String, NilClass),
|
25
|
-
tags: T::Hash[Symbol, T.untyped],
|
26
|
-
context: String
|
27
|
-
).void
|
28
|
-
end
|
29
|
-
|
30
12
|
def mark(key, action, step, tags, context)
|
31
13
|
marker = {
|
32
14
|
key: key,
|
@@ -47,14 +29,6 @@ module Statsig
|
|
47
29
|
@markers[context].push(marker)
|
48
30
|
end
|
49
31
|
|
50
|
-
sig do
|
51
|
-
params(
|
52
|
-
context: String,
|
53
|
-
key: String,
|
54
|
-
step: T.any(String, NilClass),
|
55
|
-
tags: T::Hash[Symbol, T.untyped]
|
56
|
-
).returns(Tracker)
|
57
|
-
end
|
58
32
|
def track(context, key, step = nil, tags = {})
|
59
33
|
tracker = Tracker.new(self, context, key, step, tags)
|
60
34
|
tracker.start(**tags)
|
@@ -87,17 +61,6 @@ module Statsig
|
|
87
61
|
API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
|
88
62
|
|
89
63
|
class Tracker
|
90
|
-
extend T::Sig
|
91
|
-
|
92
|
-
sig do
|
93
|
-
params(
|
94
|
-
diagnostics: Diagnostics,
|
95
|
-
context: String,
|
96
|
-
key: String,
|
97
|
-
step: T.any(String, NilClass),
|
98
|
-
tags: T::Hash[Symbol, T.untyped]
|
99
|
-
).void
|
100
|
-
end
|
101
64
|
def initialize(diagnostics, context, key, step, tags = {})
|
102
65
|
@diagnostics = diagnostics
|
103
66
|
@context = context
|
@@ -115,4 +78,4 @@ module Statsig
|
|
115
78
|
end
|
116
79
|
end
|
117
80
|
end
|
118
|
-
end
|
81
|
+
end
|
data/lib/dynamic_config.rb
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
# typed: false
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
|
-
|
5
1
|
##
|
6
2
|
# Contains the current experiment/dynamic config values from Statsig
|
7
3
|
#
|
@@ -9,46 +5,28 @@ require 'sorbet-runtime'
|
|
9
5
|
#
|
10
6
|
# Experiments Documentation: https://docs.statsig.com/experiments-plus
|
11
7
|
class DynamicConfig
|
12
|
-
extend T::Sig
|
13
8
|
|
14
|
-
sig { returns(String) }
|
15
9
|
attr_accessor :name
|
16
10
|
|
17
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
18
11
|
attr_accessor :value
|
19
12
|
|
20
|
-
sig { returns(String) }
|
21
13
|
attr_accessor :rule_id
|
22
14
|
|
23
|
-
sig { returns(T.nilable(String)) }
|
24
15
|
attr_accessor :group_name
|
25
16
|
|
26
|
-
sig { returns(String) }
|
27
17
|
attr_accessor :id_type
|
28
18
|
|
29
|
-
sig { returns(T.nilable(Statsig::EvaluationDetails)) }
|
30
19
|
attr_accessor :evaluation_details
|
31
20
|
|
32
|
-
sig do
|
33
|
-
params(
|
34
|
-
name: String,
|
35
|
-
value: T::Hash[String, T.untyped],
|
36
|
-
rule_id: String,
|
37
|
-
group_name: T.nilable(String),
|
38
|
-
id_type: String,
|
39
|
-
evaluation_details: T.nilable(Statsig::EvaluationDetails)
|
40
|
-
).void
|
41
|
-
end
|
42
21
|
def initialize(name, value = {}, rule_id = '', group_name = nil, id_type = '', evaluation_details = nil)
|
43
22
|
@name = name
|
44
|
-
@value = value
|
23
|
+
@value = value || {}
|
45
24
|
@rule_id = rule_id
|
46
25
|
@group_name = group_name
|
47
26
|
@id_type = id_type
|
48
27
|
@evaluation_details = evaluation_details
|
49
28
|
end
|
50
29
|
|
51
|
-
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
52
30
|
##
|
53
31
|
# Get the value for the given key (index), falling back to the default_value if it cannot be found.
|
54
32
|
#
|
@@ -59,7 +37,6 @@ class DynamicConfig
|
|
59
37
|
@value[index]
|
60
38
|
end
|
61
39
|
|
62
|
-
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
63
40
|
##
|
64
41
|
# Get the value for the given key (index), falling back to the default_value if it cannot be found
|
65
42
|
# or is found to have a different type from the default_value.
|
data/lib/error_boundary.rb
CHANGED
@@ -1,15 +1,10 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
1
|
require 'statsig_errors'
|
4
|
-
require 'sorbet-runtime'
|
5
2
|
|
6
3
|
$endpoint = 'https://statsigapi.net/v1/sdk_exception'
|
7
4
|
|
8
5
|
module Statsig
|
9
6
|
class ErrorBoundary
|
10
|
-
extend T::Sig
|
11
7
|
|
12
|
-
sig { params(sdk_key: String).void }
|
13
8
|
def initialize(sdk_key)
|
14
9
|
@sdk_key = sdk_key
|
15
10
|
@seen = Set.new
|
data/lib/evaluation_details.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# typed: true
|
2
1
|
module Statsig
|
3
2
|
|
4
3
|
module EvaluationReason
|
@@ -25,6 +24,10 @@ module Statsig
|
|
25
24
|
@server_time = (Time.now.to_i * 1000).to_s
|
26
25
|
end
|
27
26
|
|
27
|
+
def self.unsupported(config_sync_time, init_time)
|
28
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::UNSUPPORTED)
|
29
|
+
end
|
30
|
+
|
28
31
|
def self.unrecognized(config_sync_time, init_time)
|
29
32
|
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::UNRECOGNIZED)
|
30
33
|
end
|