statsig 1.31.1 → 1.32.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 +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
|