statsig 1.6.3 → 1.8.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/evaluation_helpers.rb +4 -4
- data/lib/evaluator.rb +53 -24
- data/lib/network.rb +8 -33
- data/lib/spec_store.rb +120 -21
- data/lib/statsig.rb +13 -6
- data/lib/statsig_driver.rb +6 -25
- data/lib/statsig_logger.rb +4 -5
- data/lib/statsig_user.rb +6 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4268658d92961175796c877e357cc3cf18f40656cf8aa272034fd4953bdb641e
|
4
|
+
data.tar.gz: e0a62e60cdca3b8b7ef8fe0302032badb88f8b9b5d86635bf20f79dc7fc011da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e40a7d816b2c36ec211272bfa21e1bacb035ae7a6a9a1366eb4bae9b0af917f1694f66a1d35f146018b49be6c0df1ec07fe549612d3a49e54c69ffeeb7e3e733
|
7
|
+
data.tar.gz: a53f398b7574bdaa47f26b5dcda0d2e11f83fbc85401b071592198ae24b59dbaec21cc74ec04f102ad909a06aeb0f552be1a49838241b035a2153b81c3250c04
|
data/lib/evaluation_helpers.rb
CHANGED
@@ -2,7 +2,7 @@ require 'time'
|
|
2
2
|
|
3
3
|
module EvaluationHelpers
|
4
4
|
def self.compare_numbers(a, b, func)
|
5
|
-
return false unless
|
5
|
+
return false unless is_numeric(a) && is_numeric(b)
|
6
6
|
func.call(a.to_f, b.to_f) rescue false
|
7
7
|
end
|
8
8
|
|
@@ -15,8 +15,8 @@ module EvaluationHelpers
|
|
15
15
|
|
16
16
|
def self.compare_times(a, b, func)
|
17
17
|
begin
|
18
|
-
time_1 =
|
19
|
-
time_2 =
|
18
|
+
time_1 = get_epoch_time(a)
|
19
|
+
time_2 = get_epoch_time(b)
|
20
20
|
func.call(time_1, time_2)
|
21
21
|
rescue
|
22
22
|
false
|
@@ -30,7 +30,7 @@ module EvaluationHelpers
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def self.get_epoch_time(v)
|
33
|
-
time =
|
33
|
+
time = is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
|
34
34
|
if time.year > Time.now.year + 100
|
35
35
|
# divide by 1000 when the epoch time is in milliseconds instead of seconds
|
36
36
|
return time.to_i / 1000
|
data/lib/evaluator.rb
CHANGED
@@ -11,36 +11,41 @@ $fetch_from_server = :fetch_from_server
|
|
11
11
|
$type_dynamic_config = 'dynamic_config'
|
12
12
|
|
13
13
|
class Evaluator
|
14
|
-
def initialize(
|
15
|
-
@spec_store =
|
16
|
-
@initialized = true
|
14
|
+
def initialize(network, error_callback)
|
15
|
+
@spec_store = SpecStore.new(network, error_callback)
|
17
16
|
@ua_parser = UserAgentParser::Parser.new
|
18
17
|
CountryLookup.initialize
|
18
|
+
@initialized = true
|
19
19
|
end
|
20
20
|
|
21
21
|
def check_gate(user, gate_name)
|
22
22
|
return nil unless @initialized && @spec_store.has_gate?(gate_name)
|
23
|
-
|
23
|
+
eval_spec(user, @spec_store.get_gate(gate_name))
|
24
24
|
end
|
25
25
|
|
26
26
|
def get_config(user, config_name)
|
27
27
|
return nil unless @initialized && @spec_store.has_config?(config_name)
|
28
|
-
|
28
|
+
eval_spec(user, @spec_store.get_config(config_name))
|
29
|
+
end
|
30
|
+
|
31
|
+
def shutdown
|
32
|
+
@spec_store.shutdown
|
29
33
|
end
|
30
34
|
|
31
35
|
private
|
32
36
|
|
33
37
|
def eval_spec(user, config)
|
38
|
+
default_rule_id = 'default'
|
39
|
+
exposures = []
|
34
40
|
if config['enabled']
|
35
|
-
exposures = []
|
36
41
|
i = 0
|
37
42
|
until i >= config['rules'].length do
|
38
43
|
rule = config['rules'][i]
|
39
|
-
result =
|
44
|
+
result = eval_rule(user, rule)
|
40
45
|
return $fetch_from_server if result == $fetch_from_server
|
41
|
-
exposures = exposures + result[
|
46
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
42
47
|
if result['value']
|
43
|
-
pass =
|
48
|
+
pass = eval_pass_percent(user, rule, config['salt'])
|
44
49
|
return ConfigResult.new(
|
45
50
|
config['name'],
|
46
51
|
pass,
|
@@ -52,9 +57,11 @@ class Evaluator
|
|
52
57
|
|
53
58
|
i += 1
|
54
59
|
end
|
60
|
+
else
|
61
|
+
default_rule_id = 'disabled'
|
55
62
|
end
|
56
63
|
|
57
|
-
ConfigResult.new(config['name'], false, config['defaultValue'],
|
64
|
+
ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
|
58
65
|
end
|
59
66
|
|
60
67
|
def eval_rule(user, rule)
|
@@ -62,20 +69,20 @@ class Evaluator
|
|
62
69
|
pass = true
|
63
70
|
i = 0
|
64
71
|
until i >= rule['conditions'].length do
|
65
|
-
result =
|
72
|
+
result = eval_condition(user, rule['conditions'][i])
|
66
73
|
if result == $fetch_from_server
|
67
74
|
return $fetch_from_server
|
68
75
|
end
|
69
76
|
|
70
77
|
if result.is_a?(Hash)
|
71
|
-
exposures = exposures + result[
|
72
|
-
pass = false if result[
|
78
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
79
|
+
pass = false if result['value'] == false
|
73
80
|
elsif result == false
|
74
81
|
pass = false
|
75
82
|
end
|
76
83
|
i += 1
|
77
84
|
end
|
78
|
-
{
|
85
|
+
{ 'value' => pass, 'exposures' => exposures }
|
79
86
|
end
|
80
87
|
|
81
88
|
def eval_condition(user, condition)
|
@@ -86,6 +93,7 @@ class Evaluator
|
|
86
93
|
operator = condition['operator']
|
87
94
|
additional_values = condition['additionalValues']
|
88
95
|
additional_values = Hash.new unless additional_values.is_a? Hash
|
96
|
+
idType = condition['idType']
|
89
97
|
|
90
98
|
return $fetch_from_server unless type.is_a? String
|
91
99
|
type = type.downcase
|
@@ -94,19 +102,19 @@ class Evaluator
|
|
94
102
|
when 'public'
|
95
103
|
return true
|
96
104
|
when 'fail_gate', 'pass_gate'
|
97
|
-
other_gate_result =
|
105
|
+
other_gate_result = check_gate(user, target)
|
98
106
|
return $fetch_from_server if other_gate_result == $fetch_from_server
|
99
107
|
|
100
108
|
gate_value = other_gate_result&.gate_value == true
|
101
109
|
new_exposure = {
|
102
|
-
|
103
|
-
|
104
|
-
|
110
|
+
'gate' => target,
|
111
|
+
'gateValue' => gate_value ? 'true' : 'false',
|
112
|
+
'ruleID' => other_gate_result&.rule_id
|
105
113
|
}
|
106
114
|
exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
|
107
115
|
return {
|
108
|
-
|
109
|
-
|
116
|
+
'value' => type == 'pass_gate' ? gate_value : !gate_value,
|
117
|
+
'exposures' => exposures
|
110
118
|
}
|
111
119
|
when 'ip_based'
|
112
120
|
value = get_value_from_user(user, field) || get_value_from_ip(user, field)
|
@@ -123,12 +131,14 @@ class Evaluator
|
|
123
131
|
when 'user_bucket'
|
124
132
|
begin
|
125
133
|
salt = additional_values['salt']
|
126
|
-
|
134
|
+
unit_id = get_unit_id(user, idType) || ''
|
127
135
|
# there are only 1000 user buckets as opposed to 10k for gate pass %
|
128
|
-
value = compute_user_hash("#{salt}.#{
|
136
|
+
value = compute_user_hash("#{salt}.#{unit_id}") % 1000
|
129
137
|
rescue
|
130
138
|
return false
|
131
139
|
end
|
140
|
+
when 'unit_id'
|
141
|
+
value = get_unit_id(user, idType)
|
132
142
|
else
|
133
143
|
return $fetch_from_server
|
134
144
|
end
|
@@ -194,6 +204,17 @@ class Evaluator
|
|
194
204
|
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
|
195
205
|
when 'on'
|
196
206
|
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
|
207
|
+
when 'in_segment_list', 'not_in_segment_list'
|
208
|
+
begin
|
209
|
+
id_list = (@spec_store.get_id_list(target) || {:ids => {}})[:ids]
|
210
|
+
hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
|
211
|
+
is_in_list = id_list.is_a?(Hash) && id_list[hashed_id] == true
|
212
|
+
|
213
|
+
return is_in_list if operator == 'in_segment_list'
|
214
|
+
return !is_in_list
|
215
|
+
rescue StandardError => e
|
216
|
+
return false
|
217
|
+
end
|
197
218
|
else
|
198
219
|
return $fetch_from_server
|
199
220
|
end
|
@@ -265,15 +286,23 @@ class Evaluator
|
|
265
286
|
def eval_pass_percent(user, rule, config_salt)
|
266
287
|
return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
|
267
288
|
begin
|
268
|
-
|
289
|
+
unit_id = get_unit_id(user, rule['id_type']) || ''
|
269
290
|
rule_salt = rule['salt'] || rule['id'] || ''
|
270
|
-
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{
|
291
|
+
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
271
292
|
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
272
293
|
rescue
|
273
294
|
return false
|
274
295
|
end
|
275
296
|
end
|
276
297
|
|
298
|
+
def get_unit_id(user, id_type)
|
299
|
+
if id_type.is_a?(String) && id_type.downcase != 'userid'
|
300
|
+
return nil unless user&.custom_ids.is_a? Hash
|
301
|
+
return user.custom_ids[id_type] || user.custom_ids[id_type.downcase]
|
302
|
+
end
|
303
|
+
user.user_id
|
304
|
+
end
|
305
|
+
|
277
306
|
def compute_user_hash(user_hash)
|
278
307
|
Digest::SHA256.digest(user_hash).unpack('Q>')[0]
|
279
308
|
end
|
data/lib/network.rb
CHANGED
@@ -12,7 +12,6 @@ class Network
|
|
12
12
|
end
|
13
13
|
@server_secret = server_secret
|
14
14
|
@api = api
|
15
|
-
@last_sync_time = 0
|
16
15
|
@backoff_multiplier = backoff_mult
|
17
16
|
end
|
18
17
|
|
@@ -24,14 +23,14 @@ class Network
|
|
24
23
|
}).accept(:json)
|
25
24
|
begin
|
26
25
|
res = http.post(@api + endpoint, body: body)
|
27
|
-
rescue
|
26
|
+
rescue StandardError => e
|
28
27
|
## network error retry
|
29
|
-
return nil unless retries > 0
|
28
|
+
return nil, e unless retries > 0
|
30
29
|
sleep backoff
|
31
30
|
return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
32
31
|
end
|
33
|
-
return res unless !res.status.success?
|
34
|
-
return nil unless retries > 0 && $retry_codes.include?(res.code)
|
32
|
+
return res, nil unless !res.status.success?
|
33
|
+
return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
|
35
34
|
## status code retry
|
36
35
|
sleep backoff
|
37
36
|
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
@@ -40,7 +39,7 @@ class Network
|
|
40
39
|
def check_gate(user, gate_name)
|
41
40
|
begin
|
42
41
|
request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
|
43
|
-
response = post_helper('check_gate', request_body)
|
42
|
+
response, _ = post_helper('check_gate', request_body)
|
44
43
|
return JSON.parse(response.body) unless response.nil?
|
45
44
|
false
|
46
45
|
rescue
|
@@ -51,7 +50,7 @@ class Network
|
|
51
50
|
def get_config(user, dynamic_config_name)
|
52
51
|
begin
|
53
52
|
request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
|
54
|
-
response = post_helper('get_config', request_body)
|
53
|
+
response, _ = post_helper('get_config', request_body)
|
55
54
|
return JSON.parse(response.body) unless response.nil?
|
56
55
|
nil
|
57
56
|
rescue
|
@@ -59,33 +58,9 @@ class Network
|
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
62
|
-
def
|
63
|
-
begin
|
64
|
-
response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
|
65
|
-
return nil unless !response.nil?
|
66
|
-
json_body = JSON.parse(response.body)
|
67
|
-
@last_sync_time = json_body['time']
|
68
|
-
return json_body
|
69
|
-
rescue
|
70
|
-
return nil
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def poll_for_changes(callback)
|
75
|
-
Thread.new do
|
76
|
-
loop do
|
77
|
-
sleep 10
|
78
|
-
specs = download_config_specs
|
79
|
-
unless specs.nil?
|
80
|
-
callback.call(specs)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def post_logs(events, statsig_metadata)
|
61
|
+
def post_logs(events)
|
87
62
|
begin
|
88
|
-
json_body = JSON.generate({'events' => events, 'statsigMetadata' =>
|
63
|
+
json_body = JSON.generate({'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata})
|
89
64
|
post_helper('log_event', json_body, retries: 5)
|
90
65
|
rescue
|
91
66
|
end
|
data/lib/spec_store.rb
CHANGED
@@ -2,40 +2,35 @@ require 'net/http'
|
|
2
2
|
require 'uri'
|
3
3
|
|
4
4
|
class SpecStore
|
5
|
-
def initialize(
|
5
|
+
def initialize(network, error_callback = nil, config_sync_interval = 10, id_lists_sync_interval = 60)
|
6
|
+
@network = network
|
6
7
|
@last_sync_time = 0
|
8
|
+
@config_sync_interval = config_sync_interval
|
9
|
+
@id_lists_sync_interval = id_lists_sync_interval
|
7
10
|
@store = {
|
8
11
|
:gates => {},
|
9
12
|
:configs => {},
|
13
|
+
:id_lists => {},
|
10
14
|
}
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
def process(specs_json)
|
15
|
-
if specs_json.nil?
|
16
|
-
return
|
17
|
-
end
|
15
|
+
e = download_config_specs
|
16
|
+
error_callback.call(e) unless error_callback.nil?
|
17
|
+
download_id_lists
|
18
18
|
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
!specs_json['dynamic_configs'].nil?
|
23
|
-
|
24
|
-
@store = {
|
25
|
-
:gates => {},
|
26
|
-
:configs => {},
|
27
|
-
}
|
19
|
+
@config_sync_thread = sync_config_specs
|
20
|
+
@id_lists_sync_thread = sync_id_lists
|
21
|
+
end
|
28
22
|
|
29
|
-
|
30
|
-
|
23
|
+
def shutdown
|
24
|
+
@config_sync_thread&.exit
|
25
|
+
@id_lists_sync_thread&.exit
|
31
26
|
end
|
32
27
|
|
33
28
|
def has_gate?(gate_name)
|
34
|
-
|
29
|
+
@store[:gates].key?(gate_name)
|
35
30
|
end
|
36
31
|
|
37
32
|
def has_config?(config_name)
|
38
|
-
|
33
|
+
@store[:configs].key?(config_name)
|
39
34
|
end
|
40
35
|
|
41
36
|
def get_gate(gate_name)
|
@@ -48,4 +43,108 @@ class SpecStore
|
|
48
43
|
@store[:configs][config_name]
|
49
44
|
end
|
50
45
|
|
46
|
+
def get_id_list(list_name)
|
47
|
+
@store[:id_lists][list_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def sync_config_specs
|
53
|
+
Thread.new do
|
54
|
+
loop do
|
55
|
+
sleep @config_sync_interval
|
56
|
+
download_config_specs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def sync_id_lists
|
62
|
+
Thread.new do
|
63
|
+
loop do
|
64
|
+
sleep @id_lists_sync_interval
|
65
|
+
download_id_lists
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def download_config_specs
|
71
|
+
begin
|
72
|
+
response, e = @network.post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
|
73
|
+
if e.nil?
|
74
|
+
process(JSON.parse(response.body))
|
75
|
+
else
|
76
|
+
e
|
77
|
+
end
|
78
|
+
rescue StandardError => e
|
79
|
+
e
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def process(specs_json)
|
84
|
+
if specs_json.nil?
|
85
|
+
return
|
86
|
+
end
|
87
|
+
|
88
|
+
@last_sync_time = specs_json['time'] || @last_sync_time
|
89
|
+
return unless specs_json['has_updates'] == true &&
|
90
|
+
!specs_json['feature_gates'].nil? &&
|
91
|
+
!specs_json['dynamic_configs'].nil?
|
92
|
+
|
93
|
+
new_gates = {}
|
94
|
+
new_configs = {}
|
95
|
+
|
96
|
+
specs_json['feature_gates'].map{|gate| new_gates[gate['name']] = gate }
|
97
|
+
specs_json['dynamic_configs'].map{|config| new_configs[config['name']] = config }
|
98
|
+
@store[:gates] = new_gates
|
99
|
+
@store[:configs] = new_configs
|
100
|
+
|
101
|
+
new_id_lists = specs_json['id_lists']
|
102
|
+
if new_id_lists.is_a? Hash
|
103
|
+
new_id_lists.each do |list_name, _|
|
104
|
+
unless @store[:id_lists].key?(list_name)
|
105
|
+
@store[:id_lists][list_name] = { :ids => {}, :time => 0 }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
@store[:id_lists].each do |list_name, _|
|
110
|
+
unless new_id_lists.key?(list_name)
|
111
|
+
@store[:id_lists].delete(list_name)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def download_id_lists
|
118
|
+
if @store[:id_lists].is_a? Hash
|
119
|
+
threads = []
|
120
|
+
id_lists = @store[:id_lists]
|
121
|
+
id_lists.each do |list_name, list|
|
122
|
+
threads << Thread.new do
|
123
|
+
response, e = @network.post_helper('download_id_list', JSON.generate({'listName' => list_name, 'statsigMetadata' => Statsig.get_statsig_metadata, 'sinceTime' => list['time'] || 0 }))
|
124
|
+
if e.nil? && !response.nil?
|
125
|
+
begin
|
126
|
+
data = JSON.parse(response)
|
127
|
+
if data['add_ids'].is_a? Array
|
128
|
+
data['add_ids'].each do |id|
|
129
|
+
list[:ids][id] = true
|
130
|
+
end
|
131
|
+
end
|
132
|
+
if data['remove_ids'].is_a? Array
|
133
|
+
data['remove_ids'].each do |id|
|
134
|
+
list[:ids]&.delete(id)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
if data['time'].is_a? Numeric
|
138
|
+
list[:time] = data['time']
|
139
|
+
end
|
140
|
+
rescue
|
141
|
+
# Ignored
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
threads.each(&:join)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
51
150
|
end
|
data/lib/statsig.rb
CHANGED
@@ -1,32 +1,32 @@
|
|
1
1
|
require 'statsig_driver'
|
2
2
|
|
3
3
|
module Statsig
|
4
|
-
def self.initialize(secret_key, options = nil)
|
4
|
+
def self.initialize(secret_key, options = nil, error_callback = nil)
|
5
5
|
unless @shared_instance.nil?
|
6
6
|
puts 'Statsig already initialized.'
|
7
7
|
return @shared_instance
|
8
8
|
end
|
9
9
|
|
10
|
-
@shared_instance = StatsigDriver.new(secret_key, options)
|
10
|
+
@shared_instance = StatsigDriver.new(secret_key, options, error_callback)
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.check_gate(user, gate_name)
|
14
|
-
|
14
|
+
ensure_initialized
|
15
15
|
@shared_instance&.check_gate(user, gate_name)
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.get_config(user, dynamic_config_name)
|
19
|
-
|
19
|
+
ensure_initialized
|
20
20
|
@shared_instance&.get_config(user, dynamic_config_name)
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.get_experiment(user, experiment_name)
|
24
|
-
|
24
|
+
ensure_initialized
|
25
25
|
@shared_instance&.get_config(user, experiment_name)
|
26
26
|
end
|
27
27
|
|
28
28
|
def self.log_event(user, event_name, value, metadata)
|
29
|
-
|
29
|
+
ensure_initialized
|
30
30
|
@shared_instance&.log_event(user, event_name, value, metadata)
|
31
31
|
end
|
32
32
|
|
@@ -37,6 +37,13 @@ module Statsig
|
|
37
37
|
@shared_instance = nil
|
38
38
|
end
|
39
39
|
|
40
|
+
def self.get_statsig_metadata
|
41
|
+
{
|
42
|
+
'sdkType' => 'ruby-server',
|
43
|
+
'sdkVersion' => '1.8.0',
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
40
47
|
private
|
41
48
|
|
42
49
|
def self.ensure_initialized
|
data/lib/statsig_driver.rb
CHANGED
@@ -8,7 +8,7 @@ require 'statsig_user'
|
|
8
8
|
require 'spec_store'
|
9
9
|
|
10
10
|
class StatsigDriver
|
11
|
-
def initialize(secret_key, options = nil)
|
11
|
+
def initialize(secret_key, options = nil, error_callback = nil)
|
12
12
|
super()
|
13
13
|
if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
|
14
14
|
raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
|
@@ -17,25 +17,12 @@ class StatsigDriver
|
|
17
17
|
raise 'Invalid options provided. Either provide a valid StatsigOptions object or nil'
|
18
18
|
end
|
19
19
|
|
20
|
-
@options = options || StatsigOptions.new
|
20
|
+
@options = options || StatsigOptions.new
|
21
21
|
@shutdown = false
|
22
22
|
@secret_key = secret_key
|
23
23
|
@net = Network.new(secret_key, @options.api_url_base)
|
24
|
-
@
|
25
|
-
|
26
|
-
'sdkVersion' => Gem::Specification::load('statsig.gemspec')&.version,
|
27
|
-
}
|
28
|
-
@logger = StatsigLogger.new(@net, @statsig_metadata)
|
29
|
-
|
30
|
-
downloaded_specs = @net.download_config_specs
|
31
|
-
unless downloaded_specs.nil?
|
32
|
-
@initialized = true
|
33
|
-
end
|
34
|
-
|
35
|
-
@store = SpecStore.new(downloaded_specs)
|
36
|
-
@evaluator = Evaluator.new(@store)
|
37
|
-
|
38
|
-
@polling_thread = @net.poll_for_changes(-> (config_specs) { @store.process(config_specs) })
|
24
|
+
@logger = StatsigLogger.new(@net)
|
25
|
+
@evaluator = Evaluator.new(@net, error_callback)
|
39
26
|
end
|
40
27
|
|
41
28
|
def check_gate(user, gate_name)
|
@@ -45,9 +32,6 @@ class StatsigDriver
|
|
45
32
|
raise 'Invalid gate_name provided'
|
46
33
|
end
|
47
34
|
check_shutdown
|
48
|
-
unless @initialized
|
49
|
-
return false
|
50
|
-
end
|
51
35
|
|
52
36
|
res = @evaluator.check_gate(user, gate_name)
|
53
37
|
if res.nil?
|
@@ -71,9 +55,6 @@ class StatsigDriver
|
|
71
55
|
raise "Invalid dynamic_config_name provided"
|
72
56
|
end
|
73
57
|
check_shutdown
|
74
|
-
unless @initialized
|
75
|
-
return DynamicConfig.new(dynamic_config_name)
|
76
|
-
end
|
77
58
|
|
78
59
|
res = @evaluator.get_config(user, dynamic_config_name)
|
79
60
|
if res.nil?
|
@@ -109,14 +90,14 @@ class StatsigDriver
|
|
109
90
|
event.user = user
|
110
91
|
event.value = value
|
111
92
|
event.metadata = metadata
|
112
|
-
event.statsig_metadata =
|
93
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
113
94
|
@logger.log_event(event)
|
114
95
|
end
|
115
96
|
|
116
97
|
def shutdown
|
117
98
|
@shutdown = true
|
118
99
|
@logger.flush(true)
|
119
|
-
@
|
100
|
+
@evaluator.shutdown
|
120
101
|
end
|
121
102
|
|
122
103
|
private
|
data/lib/statsig_logger.rb
CHANGED
@@ -4,9 +4,8 @@ $gate_exposure_event = 'statsig::gate_exposure'
|
|
4
4
|
$config_exposure_event = 'statsig::config_exposure'
|
5
5
|
|
6
6
|
class StatsigLogger
|
7
|
-
def initialize(network
|
7
|
+
def initialize(network)
|
8
8
|
@network = network
|
9
|
-
@statsig_metadata = statsig_metadata
|
10
9
|
@events = []
|
11
10
|
@background_flush = Thread.new do
|
12
11
|
sleep 60
|
@@ -29,7 +28,7 @@ class StatsigLogger
|
|
29
28
|
'gateValue' => value.to_s,
|
30
29
|
'ruleID' => rule_id
|
31
30
|
}
|
32
|
-
event.statsig_metadata =
|
31
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
33
32
|
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
34
33
|
log_event(event)
|
35
34
|
end
|
@@ -41,7 +40,7 @@ class StatsigLogger
|
|
41
40
|
'config' => config_name,
|
42
41
|
'ruleID' => rule_id
|
43
42
|
}
|
44
|
-
event.statsig_metadata =
|
43
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
45
44
|
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
46
45
|
log_event(event)
|
47
46
|
end
|
@@ -56,6 +55,6 @@ class StatsigLogger
|
|
56
55
|
flush_events = @events.map { |e| e.serialize }
|
57
56
|
@events = []
|
58
57
|
|
59
|
-
@network.post_logs(flush_events
|
58
|
+
@network.post_logs(flush_events)
|
60
59
|
end
|
61
60
|
end
|
data/lib/statsig_user.rb
CHANGED
@@ -7,6 +7,7 @@ class StatsigUser
|
|
7
7
|
attr_accessor :locale
|
8
8
|
attr_accessor :app_version
|
9
9
|
attr_accessor :statsig_environment
|
10
|
+
attr_accessor :custom_ids
|
10
11
|
attr_accessor :private_attributes
|
11
12
|
|
12
13
|
def custom
|
@@ -28,9 +29,11 @@ class StatsigUser
|
|
28
29
|
@country = user_hash['country']
|
29
30
|
@locale = user_hash['locale']
|
30
31
|
@app_version = user_hash['appVersion'] || user_hash['app_version']
|
31
|
-
@custom = user_hash['custom']
|
32
|
+
@custom = user_hash['custom'] if user_hash['custom'].is_a? Hash
|
32
33
|
@statsig_environment = user_hash['statsigEnvironment']
|
33
|
-
@private_attributes = user_hash['privateAttributes']
|
34
|
+
@private_attributes = user_hash['privateAttributes'] if user_hash['privateAttributes'].is_a? Hash
|
35
|
+
custom_ids = user_hash['customIDs'] || user_hash['custom_ids']
|
36
|
+
@custom_ids = custom_ids if custom_ids.is_a? Hash
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
@@ -46,6 +49,7 @@ class StatsigUser
|
|
46
49
|
'custom' => @custom,
|
47
50
|
'statsigEnvironment' => @statsig_environment,
|
48
51
|
'privateAttributes' => @private_attributes,
|
52
|
+
'customIDs' => @custom_ids,
|
49
53
|
}
|
50
54
|
if for_logging
|
51
55
|
hash.delete('privateAttributes')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: statsig
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Statsig, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|