statsig 1.7.0 → 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/evaluator.rb +35 -8
- data/lib/network.rb +2 -27
- data/lib/spec_store.rb +120 -21
- data/lib/statsig.rb +7 -0
- data/lib/statsig_driver.rb +4 -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/evaluator.rb
CHANGED
@@ -11,11 +11,11 @@ $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)
|
@@ -28,6 +28,10 @@ class Evaluator
|
|
28
28
|
eval_spec(user, @spec_store.get_config(config_name))
|
29
29
|
end
|
30
30
|
|
31
|
+
def shutdown
|
32
|
+
@spec_store.shutdown
|
33
|
+
end
|
34
|
+
|
31
35
|
private
|
32
36
|
|
33
37
|
def eval_spec(user, config)
|
@@ -53,7 +57,8 @@ class Evaluator
|
|
53
57
|
|
54
58
|
i += 1
|
55
59
|
end
|
56
|
-
|
60
|
+
else
|
61
|
+
default_rule_id = 'disabled'
|
57
62
|
end
|
58
63
|
|
59
64
|
ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
|
@@ -88,6 +93,7 @@ class Evaluator
|
|
88
93
|
operator = condition['operator']
|
89
94
|
additional_values = condition['additionalValues']
|
90
95
|
additional_values = Hash.new unless additional_values.is_a? Hash
|
96
|
+
idType = condition['idType']
|
91
97
|
|
92
98
|
return $fetch_from_server unless type.is_a? String
|
93
99
|
type = type.downcase
|
@@ -125,12 +131,14 @@ class Evaluator
|
|
125
131
|
when 'user_bucket'
|
126
132
|
begin
|
127
133
|
salt = additional_values['salt']
|
128
|
-
|
134
|
+
unit_id = get_unit_id(user, idType) || ''
|
129
135
|
# there are only 1000 user buckets as opposed to 10k for gate pass %
|
130
|
-
value = compute_user_hash("#{salt}.#{
|
136
|
+
value = compute_user_hash("#{salt}.#{unit_id}") % 1000
|
131
137
|
rescue
|
132
138
|
return false
|
133
139
|
end
|
140
|
+
when 'unit_id'
|
141
|
+
value = get_unit_id(user, idType)
|
134
142
|
else
|
135
143
|
return $fetch_from_server
|
136
144
|
end
|
@@ -196,6 +204,17 @@ class Evaluator
|
|
196
204
|
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
|
197
205
|
when 'on'
|
198
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
|
199
218
|
else
|
200
219
|
return $fetch_from_server
|
201
220
|
end
|
@@ -267,15 +286,23 @@ class Evaluator
|
|
267
286
|
def eval_pass_percent(user, rule, config_salt)
|
268
287
|
return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
|
269
288
|
begin
|
270
|
-
|
289
|
+
unit_id = get_unit_id(user, rule['id_type']) || ''
|
271
290
|
rule_salt = rule['salt'] || rule['id'] || ''
|
272
|
-
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{
|
291
|
+
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
273
292
|
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
274
293
|
rescue
|
275
294
|
return false
|
276
295
|
end
|
277
296
|
end
|
278
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
|
+
|
279
306
|
def compute_user_hash(user_hash)
|
280
307
|
Digest::SHA256.digest(user_hash).unpack('Q>')[0]
|
281
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
|
|
@@ -59,33 +58,9 @@ class Network
|
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
62
|
-
def
|
61
|
+
def post_logs(events)
|
63
62
|
begin
|
64
|
-
|
65
|
-
return nil, e if response.nil?
|
66
|
-
json_body = JSON.parse(response.body)
|
67
|
-
@last_sync_time = json_body['time']
|
68
|
-
return json_body, nil
|
69
|
-
rescue StandardError => e
|
70
|
-
return nil, e
|
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)
|
87
|
-
begin
|
88
|
-
json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
|
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
data/lib/statsig_driver.rb
CHANGED
@@ -21,23 +21,8 @@ class StatsigDriver
|
|
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, e = @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) })
|
39
|
-
|
40
|
-
error_callback.call(e) unless error_callback.nil?
|
24
|
+
@logger = StatsigLogger.new(@net)
|
25
|
+
@evaluator = Evaluator.new(@net, error_callback)
|
41
26
|
end
|
42
27
|
|
43
28
|
def check_gate(user, gate_name)
|
@@ -47,9 +32,6 @@ class StatsigDriver
|
|
47
32
|
raise 'Invalid gate_name provided'
|
48
33
|
end
|
49
34
|
check_shutdown
|
50
|
-
unless @initialized
|
51
|
-
return false
|
52
|
-
end
|
53
35
|
|
54
36
|
res = @evaluator.check_gate(user, gate_name)
|
55
37
|
if res.nil?
|
@@ -73,9 +55,6 @@ class StatsigDriver
|
|
73
55
|
raise "Invalid dynamic_config_name provided"
|
74
56
|
end
|
75
57
|
check_shutdown
|
76
|
-
unless @initialized
|
77
|
-
return DynamicConfig.new(dynamic_config_name)
|
78
|
-
end
|
79
58
|
|
80
59
|
res = @evaluator.get_config(user, dynamic_config_name)
|
81
60
|
if res.nil?
|
@@ -111,14 +90,14 @@ class StatsigDriver
|
|
111
90
|
event.user = user
|
112
91
|
event.value = value
|
113
92
|
event.metadata = metadata
|
114
|
-
event.statsig_metadata =
|
93
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
115
94
|
@logger.log_event(event)
|
116
95
|
end
|
117
96
|
|
118
97
|
def shutdown
|
119
98
|
@shutdown = true
|
120
99
|
@logger.flush(true)
|
121
|
-
@
|
100
|
+
@evaluator.shutdown
|
122
101
|
end
|
123
102
|
|
124
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
|