statsig 1.25.2 → 1.30.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/client_initialize_helpers.rb +41 -5
- data/lib/config_result.rb +49 -3
- data/lib/diagnostics.rb +51 -24
- data/lib/dynamic_config.rb +15 -2
- data/lib/error_boundary.rb +32 -44
- data/lib/evaluation_details.rb +12 -7
- data/lib/evaluator.rb +79 -21
- data/lib/feature_gate.rb +70 -0
- data/lib/hash_utils.rb +32 -0
- data/lib/id_list.rb +1 -1
- data/lib/interfaces/user_persistent_storage.rb +12 -0
- data/lib/layer.rb +17 -3
- data/lib/network.rb +94 -45
- data/lib/spec_store.rb +129 -55
- data/lib/statsig.rb +121 -21
- data/lib/statsig_driver.rb +193 -93
- data/lib/statsig_errors.rb +7 -0
- data/lib/statsig_logger.rb +26 -17
- data/lib/statsig_options.rb +33 -9
- data/lib/statsig_user.rb +53 -4
- data/lib/ua_parser.rb +1 -0
- data/lib/uri_helper.rb +1 -1
- data/lib/user_persistent_storage_utils.rb +106 -0
- metadata +58 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72119a11268473774b98457f89017155813cdaab4c19b8f07e1aad370ae6dd17
|
4
|
+
data.tar.gz: 69cb96c04c6b9c322bb239332b8c406a6b543bb42b0cab6da9e641d44fa15371
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d75c0a0d9c6529843c554d05b3ae49e6d863cd01da11f932e679bddddffab4287ef036af37c3cd8fff9310c10cd6758bc6e5435f78626245c5d1f315b91e09eb
|
7
|
+
data.tar.gz: b9886d0b78d1491393cfacdea4c1f4cdeae1d2a993a0f5a0875011be1083bb63e4a7721fb3a007869eaa7eaf69ad300438d8038c099e88277958d4daf6d226f7
|
@@ -1,4 +1,8 @@
|
|
1
1
|
# typed: true
|
2
|
+
|
3
|
+
require_relative 'hash_utils'
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
|
2
6
|
$empty_eval_result = {
|
3
7
|
:gate_value => false,
|
4
8
|
:json_value => {},
|
@@ -9,10 +13,14 @@ $empty_eval_result = {
|
|
9
13
|
|
10
14
|
module ClientInitializeHelpers
|
11
15
|
class ResponseFormatter
|
12
|
-
|
16
|
+
extend T::Sig
|
17
|
+
|
18
|
+
def initialize(evaluator, user, hash, client_sdk_key)
|
13
19
|
@evaluator = evaluator
|
14
20
|
@user = user
|
15
21
|
@specs = evaluator.spec_store.get_raw_specs
|
22
|
+
@hash = hash
|
23
|
+
@client_sdk_key = client_sdk_key
|
16
24
|
end
|
17
25
|
|
18
26
|
def get_responses(key)
|
@@ -23,7 +31,21 @@ module ClientInitializeHelpers
|
|
23
31
|
|
24
32
|
private
|
25
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:')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
26
41
|
def to_response(config_name, config_spec)
|
42
|
+
target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
|
43
|
+
config_target_apps = config_spec['targetAppIDs']
|
44
|
+
|
45
|
+
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
|
27
49
|
eval_result = @evaluator.eval_spec(@user, config_spec)
|
28
50
|
if eval_result.nil?
|
29
51
|
return nil
|
@@ -33,9 +55,11 @@ module ClientInitializeHelpers
|
|
33
55
|
:gate_value => eval_result.gate_value,
|
34
56
|
:json_value => eval_result.json_value,
|
35
57
|
:rule_id => eval_result.rule_id,
|
58
|
+
:group_name => eval_result.group_name,
|
59
|
+
:id_type => eval_result.id_type,
|
36
60
|
:config_delegate => eval_result.config_delegate,
|
37
61
|
:is_experiment_group => eval_result.is_experiment_group,
|
38
|
-
:secondary_exposures => eval_result.secondary_exposures,
|
62
|
+
:secondary_exposures => filter_segments_from_secondary_exposures(eval_result.secondary_exposures),
|
39
63
|
:undelegated_sec_exps => eval_result.undelegated_sec_exps
|
40
64
|
}
|
41
65
|
|
@@ -52,10 +76,14 @@ module ClientInitializeHelpers
|
|
52
76
|
end
|
53
77
|
|
54
78
|
result['value'] = safe_eval_result[:gate_value]
|
79
|
+
result["group_name"] = safe_eval_result[:group_name]
|
80
|
+
result["id_type"] = safe_eval_result[:id_type]
|
55
81
|
when 'dynamic_config'
|
56
82
|
id_type = config_spec['idType']
|
57
83
|
result['value'] = safe_eval_result[:json_value]
|
58
84
|
result["group"] = safe_eval_result[:rule_id]
|
85
|
+
result["group_name"] = safe_eval_result[:group_name]
|
86
|
+
result["id_type"] = safe_eval_result[:id_type]
|
59
87
|
result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
|
60
88
|
else
|
61
89
|
return nil
|
@@ -67,6 +95,7 @@ module ClientInitializeHelpers
|
|
67
95
|
|
68
96
|
if entity_type == 'layer'
|
69
97
|
populate_layer_fields(config_spec, safe_eval_result, result)
|
98
|
+
result.delete('id_type') # not exposed for layer configs in /initialize
|
70
99
|
end
|
71
100
|
|
72
101
|
hashed_name = hash_name(config_name)
|
@@ -75,7 +104,7 @@ module ClientInitializeHelpers
|
|
75
104
|
"name" => hashed_name,
|
76
105
|
"rule_id" => safe_eval_result[:rule_id],
|
77
106
|
"secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
|
78
|
-
})]
|
107
|
+
}).compact]
|
79
108
|
end
|
80
109
|
|
81
110
|
def clean_exposures(exposures)
|
@@ -126,7 +155,14 @@ module ClientInitializeHelpers
|
|
126
155
|
end
|
127
156
|
|
128
157
|
def hash_name(name)
|
129
|
-
|
158
|
+
case @hash
|
159
|
+
when 'none'
|
160
|
+
return name
|
161
|
+
when 'sha256'
|
162
|
+
return Statsig::HashUtils.sha256(name)
|
163
|
+
when 'djb2'
|
164
|
+
return Statsig::HashUtils.djb2(name)
|
165
|
+
end
|
130
166
|
end
|
131
167
|
end
|
132
|
-
end
|
168
|
+
end
|
data/lib/config_result.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
# typed: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
2
5
|
module Statsig
|
3
6
|
class ConfigResult
|
7
|
+
extend T::Sig
|
8
|
+
|
4
9
|
attr_accessor :name
|
5
10
|
attr_accessor :gate_value
|
6
11
|
attr_accessor :json_value
|
@@ -13,6 +18,7 @@ module Statsig
|
|
13
18
|
attr_accessor :evaluation_details
|
14
19
|
attr_accessor :group_name
|
15
20
|
attr_accessor :id_type
|
21
|
+
attr_accessor :target_app_ids
|
16
22
|
|
17
23
|
def initialize(
|
18
24
|
name,
|
@@ -20,12 +26,13 @@ module Statsig
|
|
20
26
|
json_value = {},
|
21
27
|
rule_id = '',
|
22
28
|
secondary_exposures = [],
|
23
|
-
config_delegate =
|
29
|
+
config_delegate = nil,
|
24
30
|
explicit_parameters = [],
|
25
31
|
is_experiment_group: false,
|
26
32
|
evaluation_details: nil,
|
27
33
|
group_name: nil,
|
28
|
-
id_type: ''
|
34
|
+
id_type: '',
|
35
|
+
target_app_ids: nil)
|
29
36
|
@name = name
|
30
37
|
@gate_value = gate_value
|
31
38
|
@json_value = json_value
|
@@ -38,6 +45,45 @@ module Statsig
|
|
38
45
|
@evaluation_details = evaluation_details
|
39
46
|
@group_name = group_name
|
40
47
|
@id_type = id_type
|
48
|
+
@target_app_ids = target_app_ids
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
|
52
|
+
def self.from_user_persisted_values(config_name, user_persisted_values)
|
53
|
+
sticky_values = user_persisted_values[config_name]
|
54
|
+
return nil if sticky_values.nil?
|
55
|
+
|
56
|
+
from_hash(config_name, sticky_values)
|
57
|
+
end
|
58
|
+
|
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
|
+
def to_hash
|
76
|
+
{
|
77
|
+
json_value: @json_value,
|
78
|
+
gate_value: @gate_value,
|
79
|
+
rule_id: @rule_id,
|
80
|
+
secondary_exposures: @secondary_exposures,
|
81
|
+
config_sync_time: @evaluation_details.config_sync_time,
|
82
|
+
init_time: @init_time,
|
83
|
+
group_name: @group_name,
|
84
|
+
id_type: @id_type,
|
85
|
+
target_app_ids: @target_app_ids
|
86
|
+
}
|
41
87
|
end
|
42
88
|
end
|
43
|
-
end
|
89
|
+
end
|
data/lib/diagnostics.rb
CHANGED
@@ -7,14 +7,18 @@ module Statsig
|
|
7
7
|
extend T::Sig
|
8
8
|
|
9
9
|
sig { returns(String) }
|
10
|
-
|
10
|
+
attr_accessor :context
|
11
11
|
|
12
12
|
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
13
13
|
attr_reader :markers
|
14
14
|
|
15
|
+
sig { returns(T::Hash[String, Numeric]) }
|
16
|
+
attr_accessor :sample_rates
|
17
|
+
|
15
18
|
def initialize(context)
|
16
19
|
@context = context
|
17
20
|
@markers = []
|
21
|
+
@sample_rates = {}
|
18
22
|
end
|
19
23
|
|
20
24
|
sig do
|
@@ -22,33 +26,37 @@ module Statsig
|
|
22
26
|
key: String,
|
23
27
|
action: String,
|
24
28
|
step: T.any(String, NilClass),
|
25
|
-
|
26
|
-
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
29
|
+
tags: T::Hash[Symbol, T.untyped]
|
27
30
|
).void
|
28
31
|
end
|
29
32
|
|
30
|
-
def mark(key, action, step
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
def mark(key, action, step, tags)
|
34
|
+
marker = {
|
35
|
+
key: key,
|
36
|
+
action: action,
|
37
|
+
timestamp: (Time.now.to_f * 1000).to_i
|
38
|
+
}
|
39
|
+
if !step.nil?
|
40
|
+
marker[:step] = step
|
41
|
+
end
|
42
|
+
tags.each do |key, val|
|
43
|
+
unless val.nil?
|
44
|
+
marker[key] = val
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@markers.push(marker)
|
39
48
|
end
|
40
49
|
|
41
50
|
sig do
|
42
51
|
params(
|
43
52
|
key: String,
|
44
53
|
step: T.any(String, NilClass),
|
45
|
-
|
46
|
-
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
54
|
+
tags: T::Hash[Symbol, T.untyped]
|
47
55
|
).returns(Tracker)
|
48
56
|
end
|
49
|
-
def track(key, step = nil,
|
50
|
-
tracker = Tracker.new(self, key, step,
|
51
|
-
tracker.start(
|
57
|
+
def track(key, step = nil, tags = {})
|
58
|
+
tracker = Tracker.new(self, key, step, tags)
|
59
|
+
tracker.start(**tags)
|
52
60
|
tracker
|
53
61
|
end
|
54
62
|
|
@@ -61,10 +69,29 @@ module Statsig
|
|
61
69
|
}
|
62
70
|
end
|
63
71
|
|
72
|
+
def serialize_with_sampling
|
73
|
+
marker_keys = @markers.map { |e| e[:key] }
|
74
|
+
unique_marker_keys = marker_keys.uniq { |e| e }
|
75
|
+
sampled_marker_keys = unique_marker_keys.select do |key|
|
76
|
+
@sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
|
77
|
+
end
|
78
|
+
final_markers = @markers.select do |marker|
|
79
|
+
!sampled_marker_keys.include?(marker[:key])
|
80
|
+
end
|
81
|
+
{
|
82
|
+
context: @context.clone,
|
83
|
+
markers: final_markers
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
64
87
|
def clear_markers
|
65
88
|
@markers.clear
|
66
89
|
end
|
67
90
|
|
91
|
+
def self.sample(rate_over_ten_thousand)
|
92
|
+
rand * 10_000 < rate_over_ten_thousand
|
93
|
+
end
|
94
|
+
|
68
95
|
class Context
|
69
96
|
INITIALIZE = 'initialize'.freeze
|
70
97
|
CONFIG_SYNC = 'config_sync'.freeze
|
@@ -81,22 +108,22 @@ module Statsig
|
|
81
108
|
diagnostics: Diagnostics,
|
82
109
|
key: String,
|
83
110
|
step: T.any(String, NilClass),
|
84
|
-
|
111
|
+
tags: T::Hash[Symbol, T.untyped]
|
85
112
|
).void
|
86
113
|
end
|
87
|
-
def initialize(diagnostics, key, step,
|
114
|
+
def initialize(diagnostics, key, step, tags = {})
|
88
115
|
@diagnostics = diagnostics
|
89
116
|
@key = key
|
90
117
|
@step = step
|
91
|
-
@
|
118
|
+
@tags = tags
|
92
119
|
end
|
93
120
|
|
94
|
-
def start(
|
95
|
-
@diagnostics.mark(@key, 'start', @step,
|
121
|
+
def start(**tags)
|
122
|
+
@diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
|
96
123
|
end
|
97
124
|
|
98
|
-
def end(
|
99
|
-
@diagnostics.mark(@key, 'end', @step,
|
125
|
+
def end(**tags)
|
126
|
+
@diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
|
100
127
|
end
|
101
128
|
end
|
102
129
|
end
|
data/lib/dynamic_config.rb
CHANGED
@@ -26,13 +26,26 @@ class DynamicConfig
|
|
26
26
|
sig { returns(String) }
|
27
27
|
attr_accessor :id_type
|
28
28
|
|
29
|
-
sig {
|
30
|
-
|
29
|
+
sig { returns(T.nilable(Statsig::EvaluationDetails)) }
|
30
|
+
attr_accessor :evaluation_details
|
31
|
+
|
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
|
+
def initialize(name, value = {}, rule_id = '', group_name = nil, id_type = '', evaluation_details = nil)
|
31
43
|
@name = name
|
32
44
|
@value = value
|
33
45
|
@rule_id = rule_id
|
34
46
|
@group_name = group_name
|
35
47
|
@id_type = id_type
|
48
|
+
@evaluation_details = evaluation_details
|
36
49
|
end
|
37
50
|
|
38
51
|
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
data/lib/error_boundary.rb
CHANGED
@@ -9,70 +9,58 @@ module Statsig
|
|
9
9
|
class ErrorBoundary
|
10
10
|
extend T::Sig
|
11
11
|
|
12
|
-
sig { returns(T.any(StatsigLogger, NilClass)) }
|
13
|
-
attr_accessor :logger
|
14
|
-
|
15
12
|
sig { params(sdk_key: String).void }
|
16
13
|
def initialize(sdk_key)
|
17
14
|
@sdk_key = sdk_key
|
18
15
|
@seen = Set.new
|
19
16
|
end
|
20
17
|
|
21
|
-
def sample_diagnostics
|
22
|
-
rand(10_000).zero?
|
23
|
-
end
|
24
|
-
|
25
18
|
def capture(task:, recover: -> {}, caller: nil)
|
26
|
-
if !caller.nil? && Diagnostics::API_CALL_KEYS.include?(caller) && sample_diagnostics
|
27
|
-
diagnostics = Diagnostics.new('api_call')
|
28
|
-
tracker = diagnostics.track(caller)
|
29
|
-
end
|
30
19
|
begin
|
31
20
|
res = task.call
|
32
|
-
|
33
|
-
|
34
|
-
tracker&.end(false)
|
35
|
-
if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
|
21
|
+
rescue StandardError, SystemStackError => e
|
22
|
+
if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
|
36
23
|
raise e
|
37
24
|
end
|
25
|
+
|
38
26
|
puts '[Statsig]: An unexpected exception occurred.'
|
39
|
-
|
27
|
+
puts e.message
|
28
|
+
log_exception(e, tag: caller)
|
40
29
|
res = recover.call
|
41
30
|
end
|
42
|
-
@logger&.log_diagnostics_event(diagnostics)
|
43
31
|
return res
|
44
32
|
end
|
45
33
|
|
46
34
|
private
|
47
35
|
|
48
|
-
def log_exception(exception)
|
49
|
-
|
50
|
-
|
51
|
-
if @seen.include?(name)
|
52
|
-
return
|
53
|
-
end
|
54
|
-
|
55
|
-
@seen << name
|
56
|
-
meta = Statsig.get_statsig_metadata
|
57
|
-
http = HTTP.headers(
|
58
|
-
{
|
59
|
-
'STATSIG-API-KEY' => @sdk_key,
|
60
|
-
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
61
|
-
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
62
|
-
'Content-Type' => 'application/json; charset=UTF-8'
|
63
|
-
}).accept(:json)
|
64
|
-
body = {
|
65
|
-
'exception' => name,
|
66
|
-
'info' => {
|
67
|
-
'trace' => exception.backtrace.to_s,
|
68
|
-
'message' => exception.message
|
69
|
-
}.to_s,
|
70
|
-
'statsigMetadata' => meta
|
71
|
-
}
|
72
|
-
http.post($endpoint, body: JSON.generate(body))
|
73
|
-
rescue
|
36
|
+
def log_exception(exception, tag: nil)
|
37
|
+
name = exception.class.name
|
38
|
+
if @seen.include?(name)
|
74
39
|
return
|
75
40
|
end
|
41
|
+
|
42
|
+
@seen << name
|
43
|
+
meta = Statsig.get_statsig_metadata
|
44
|
+
http = HTTP.headers(
|
45
|
+
{
|
46
|
+
'STATSIG-API-KEY' => @sdk_key,
|
47
|
+
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
48
|
+
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
49
|
+
'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
|
50
|
+
'Content-Type' => 'application/json; charset=UTF-8'
|
51
|
+
}).accept(:json)
|
52
|
+
body = {
|
53
|
+
'exception' => name,
|
54
|
+
'info' => {
|
55
|
+
'trace' => exception.backtrace.to_s,
|
56
|
+
'message' => exception.message
|
57
|
+
}.to_s,
|
58
|
+
'statsigMetadata' => meta,
|
59
|
+
'tag' => tag
|
60
|
+
}
|
61
|
+
http.post($endpoint, body: JSON.generate(body))
|
62
|
+
rescue StandardError
|
63
|
+
return
|
76
64
|
end
|
77
65
|
end
|
78
|
-
end
|
66
|
+
end
|
data/lib/evaluation_details.rb
CHANGED
@@ -2,12 +2,13 @@
|
|
2
2
|
module Statsig
|
3
3
|
|
4
4
|
module EvaluationReason
|
5
|
-
NETWORK =
|
6
|
-
LOCAL_OVERRIDE =
|
7
|
-
UNRECOGNIZED =
|
8
|
-
UNINITIALIZED =
|
9
|
-
BOOTSTRAP =
|
10
|
-
DATA_ADAPTER =
|
5
|
+
NETWORK = 'Network'.freeze
|
6
|
+
LOCAL_OVERRIDE = 'LocalOverride'.freeze
|
7
|
+
UNRECOGNIZED = 'Unrecognized'.freeze
|
8
|
+
UNINITIALIZED = 'Uninitialized'.freeze
|
9
|
+
BOOTSTRAP = 'Bootstrap'.freeze
|
10
|
+
DATA_ADAPTER = 'DataAdapter'.freeze
|
11
|
+
PERSISTED = 'Persisted'.freeze
|
11
12
|
end
|
12
13
|
|
13
14
|
class EvaluationDetails
|
@@ -38,5 +39,9 @@ module Statsig
|
|
38
39
|
def self.local_override(config_sync_time, init_time)
|
39
40
|
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::LOCAL_OVERRIDE)
|
40
41
|
end
|
42
|
+
|
43
|
+
def self.persisted(config_sync_time, init_time)
|
44
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::PERSISTED)
|
45
|
+
end
|
41
46
|
end
|
42
|
-
end
|
47
|
+
end
|