statsig 1.8.4 → 1.9.2
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/config_result.rb +7 -1
- data/lib/evaluator.rb +48 -20
- data/lib/id_list.rb +36 -0
- data/lib/layer.rb +21 -0
- data/lib/network.rb +3 -1
- data/lib/spec_store.rb +101 -40
- data/lib/statsig.rb +7 -2
- data/lib/statsig_driver.rb +49 -25
- data/lib/statsig_logger.rb +24 -0
- data/lib/statsig_options.rb +1 -1
- data/lib/statsig_user.rb +1 -1
- metadata +12 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: afbf6d972c5aeead4eeaa8c352155c41b02a3710f55d80309939d5acc0fd1e80
|
4
|
+
data.tar.gz: c83f0399868af1d7978aeb2d2723f4bfbc860646de3ea0a7aaf840903489b92a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: babe4ac6dc398bd87df7537ea7c9391e72e4a32c4ec80944895adb507351a02042dd2bfc28deb2336e706fcf531ba4d30dfdeeb280404b108a25505fcd749ed8
|
7
|
+
data.tar.gz: c1bad8f27c2675b7dbc125090a56062e71c5fddb6f0cc8464c0e9ea03b910f9ca918a9ccf4e6c8db99815b7964aaf543d0cc69db5a873bcf58ddc806ef8932cc
|
data/lib/config_result.rb
CHANGED
@@ -6,13 +6,19 @@ module Statsig
|
|
6
6
|
attr_accessor :json_value
|
7
7
|
attr_accessor :rule_id
|
8
8
|
attr_accessor :secondary_exposures
|
9
|
+
attr_accessor :undelegated_sec_exps
|
10
|
+
attr_accessor :config_delegate
|
11
|
+
attr_accessor :explicit_parameters
|
9
12
|
|
10
|
-
def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [])
|
13
|
+
def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [], undelegated_sec_exps = [], config_delegate = '', explicit_parameters = [])
|
11
14
|
@name = name
|
12
15
|
@gate_value = gate_value
|
13
16
|
@json_value = json_value
|
14
17
|
@rule_id = rule_id
|
15
18
|
@secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
19
|
+
@undelegated_sec_exps = undelegated_sec_exps.is_a?(Array) ? undelegated_sec_exps : []
|
20
|
+
@config_delegate = config_delegate
|
21
|
+
@explicit_parameters = explicit_parameters
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
data/lib/evaluator.rb
CHANGED
@@ -7,7 +7,7 @@ require 'time'
|
|
7
7
|
require 'user_agent_parser'
|
8
8
|
require 'user_agent_parser/operating_system'
|
9
9
|
|
10
|
-
$fetch_from_server =
|
10
|
+
$fetch_from_server = 'fetch_from_server'
|
11
11
|
$type_dynamic_config = 'dynamic_config'
|
12
12
|
|
13
13
|
module Statsig
|
@@ -29,6 +29,11 @@ module Statsig
|
|
29
29
|
eval_spec(user, @spec_store.get_config(config_name))
|
30
30
|
end
|
31
31
|
|
32
|
+
def get_layer(user, layer_name)
|
33
|
+
return nil unless @initialized && @spec_store.has_layer?(layer_name)
|
34
|
+
eval_spec(user, @spec_store.get_layer(layer_name))
|
35
|
+
end
|
36
|
+
|
32
37
|
def shutdown
|
33
38
|
@spec_store.shutdown
|
34
39
|
end
|
@@ -43,15 +48,20 @@ module Statsig
|
|
43
48
|
until i >= config['rules'].length do
|
44
49
|
rule = config['rules'][i]
|
45
50
|
result = eval_rule(user, rule)
|
46
|
-
return $fetch_from_server if result == $fetch_from_server
|
47
|
-
exposures = exposures + result
|
48
|
-
if result
|
51
|
+
return $fetch_from_server if result.to_s == $fetch_from_server
|
52
|
+
exposures = exposures + result.secondary_exposures
|
53
|
+
if result.gate_value
|
54
|
+
|
55
|
+
if (delegated_result = eval_delegate(config['name'], user, rule, exposures))
|
56
|
+
return delegated_result
|
57
|
+
end
|
58
|
+
|
49
59
|
pass = eval_pass_percent(user, rule, config['salt'])
|
50
60
|
return Statsig::ConfigResult.new(
|
51
61
|
config['name'],
|
52
62
|
pass,
|
53
|
-
pass ?
|
54
|
-
|
63
|
+
pass ? result.json_value : config['defaultValue'],
|
64
|
+
result.rule_id,
|
55
65
|
exposures
|
56
66
|
)
|
57
67
|
end
|
@@ -71,7 +81,7 @@ module Statsig
|
|
71
81
|
i = 0
|
72
82
|
until i >= rule['conditions'].length do
|
73
83
|
result = eval_condition(user, rule['conditions'][i])
|
74
|
-
if result == $fetch_from_server
|
84
|
+
if result.to_s == $fetch_from_server
|
75
85
|
return $fetch_from_server
|
76
86
|
end
|
77
87
|
|
@@ -83,7 +93,23 @@ module Statsig
|
|
83
93
|
end
|
84
94
|
i += 1
|
85
95
|
end
|
86
|
-
|
96
|
+
|
97
|
+
Statsig::ConfigResult.new('', pass, rule['returnValue'], rule['id'], exposures)
|
98
|
+
end
|
99
|
+
|
100
|
+
def eval_delegate(name, user, rule, exposures)
|
101
|
+
return nil unless (delegate = rule['configDelegate'])
|
102
|
+
return nil unless (config = @spec_store.get_config(delegate))
|
103
|
+
|
104
|
+
delegated_result = self.eval_spec(user, config)
|
105
|
+
return $fetch_from_server if delegated_result.to_s == $fetch_from_server
|
106
|
+
|
107
|
+
delegated_result.name = name
|
108
|
+
delegated_result.config_delegate = delegate
|
109
|
+
delegated_result.secondary_exposures = exposures + delegated_result.secondary_exposures
|
110
|
+
delegated_result.undelegated_sec_exps = exposures
|
111
|
+
delegated_result.explicit_parameters = config['explicitParameters']
|
112
|
+
delegated_result
|
87
113
|
end
|
88
114
|
|
89
115
|
def eval_condition(user, condition)
|
@@ -104,7 +130,7 @@ module Statsig
|
|
104
130
|
return true
|
105
131
|
when 'fail_gate', 'pass_gate'
|
106
132
|
other_gate_result = check_gate(user, target)
|
107
|
-
return $fetch_from_server if other_gate_result == $fetch_from_server
|
133
|
+
return $fetch_from_server if other_gate_result.to_s == $fetch_from_server
|
108
134
|
|
109
135
|
gate_value = other_gate_result&.gate_value == true
|
110
136
|
new_exposure = {
|
@@ -119,10 +145,10 @@ module Statsig
|
|
119
145
|
}
|
120
146
|
when 'ip_based'
|
121
147
|
value = get_value_from_user(user, field) || get_value_from_ip(user, field)
|
122
|
-
return $fetch_from_server if value == $fetch_from_server
|
148
|
+
return $fetch_from_server if value.to_s == $fetch_from_server
|
123
149
|
when 'ua_based'
|
124
150
|
value = get_value_from_user(user, field) || get_value_from_ua(user, field)
|
125
|
-
return $fetch_from_server if value == $fetch_from_server
|
151
|
+
return $fetch_from_server if value.to_s == $fetch_from_server
|
126
152
|
when 'user_field'
|
127
153
|
value = get_value_from_user(user, field)
|
128
154
|
when 'environment_field'
|
@@ -144,7 +170,7 @@ module Statsig
|
|
144
170
|
return $fetch_from_server
|
145
171
|
end
|
146
172
|
|
147
|
-
return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
|
173
|
+
return $fetch_from_server if value.to_s == $fetch_from_server || !operator.is_a?(String)
|
148
174
|
operator = operator.downcase
|
149
175
|
|
150
176
|
case operator
|
@@ -214,13 +240,15 @@ module Statsig
|
|
214
240
|
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
|
215
241
|
when 'in_segment_list', 'not_in_segment_list'
|
216
242
|
begin
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
243
|
+
is_in_list = false
|
244
|
+
id_list = @spec_store.get_id_list(target)
|
245
|
+
if id_list.is_a? IDList
|
246
|
+
hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
|
247
|
+
is_in_list = id_list.ids.include?(hashed_id)
|
248
|
+
end
|
249
|
+
return is_in_list if operator == 'in_segment_list'
|
250
|
+
return !is_in_list
|
251
|
+
rescue
|
224
252
|
return false
|
225
253
|
end
|
226
254
|
else
|
@@ -294,7 +322,7 @@ module Statsig
|
|
294
322
|
def eval_pass_percent(user, rule, config_salt)
|
295
323
|
return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
|
296
324
|
begin
|
297
|
-
unit_id = get_unit_id(user, rule['
|
325
|
+
unit_id = get_unit_id(user, rule['idType']) || ''
|
298
326
|
rule_salt = rule['salt'] || rule['id'] || ''
|
299
327
|
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
300
328
|
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
data/lib/id_list.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Statsig
|
2
|
+
class IDList
|
3
|
+
attr_accessor :name
|
4
|
+
attr_accessor :size
|
5
|
+
attr_accessor :creation_time
|
6
|
+
attr_accessor :url
|
7
|
+
attr_accessor :file_id
|
8
|
+
attr_accessor :ids
|
9
|
+
|
10
|
+
def initialize(json, ids = Set.new)
|
11
|
+
@name = json['name'] || ''
|
12
|
+
@size = json['size'] || 0
|
13
|
+
@creation_time = json['creationTime'] || 0
|
14
|
+
@url = json['url']
|
15
|
+
@file_id = json['fileID']
|
16
|
+
|
17
|
+
@ids = ids
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.new_empty(json)
|
21
|
+
self.new(json)
|
22
|
+
@size = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
return false if other.nil?
|
27
|
+
|
28
|
+
self.name == other.name &&
|
29
|
+
self.size == other.size &&
|
30
|
+
self.creation_time == other.creation_time &&
|
31
|
+
self.url == other.url &&
|
32
|
+
self.file_id == other.file_id &&
|
33
|
+
self.ids == other.ids
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/layer.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class Layer
|
2
|
+
attr_accessor :name
|
3
|
+
attr_accessor :rule_id
|
4
|
+
|
5
|
+
def initialize(name, value = {}, rule_id = '', exposure_log_func = nil)
|
6
|
+
@name = name
|
7
|
+
@value = value
|
8
|
+
@rule_id = rule_id
|
9
|
+
@exposure_log_func = exposure_log_func
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(index, default_value)
|
13
|
+
return default_value if @value.nil? || !@value.key?(index)
|
14
|
+
|
15
|
+
if @exposure_log_func.is_a? Proc
|
16
|
+
@exposure_log_func.call(self, index)
|
17
|
+
end
|
18
|
+
|
19
|
+
@value[index]
|
20
|
+
end
|
21
|
+
end
|
data/lib/network.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'http'
|
2
2
|
require 'json'
|
3
|
-
require '
|
3
|
+
require 'securerandom'
|
4
4
|
|
5
5
|
$retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
|
6
6
|
|
@@ -14,12 +14,14 @@ module Statsig
|
|
14
14
|
@server_secret = server_secret
|
15
15
|
@api = api
|
16
16
|
@backoff_multiplier = backoff_mult
|
17
|
+
@session_id = SecureRandom.uuid
|
17
18
|
end
|
18
19
|
|
19
20
|
def post_helper(endpoint, body, retries = 0, backoff = 1)
|
20
21
|
http = HTTP.headers(
|
21
22
|
{"STATSIG-API-KEY" => @server_secret,
|
22
23
|
"STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
|
24
|
+
"STATSIG-SERVER-SESSION-ID" => @session_id,
|
23
25
|
"Content-Type" => "application/json; charset=UTF-8"
|
24
26
|
}).accept(:json)
|
25
27
|
begin
|
data/lib/spec_store.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'uri'
|
3
3
|
|
4
|
+
require 'id_list'
|
5
|
+
|
4
6
|
module Statsig
|
5
7
|
class SpecStore
|
6
8
|
def initialize(network, error_callback = nil, config_sync_interval = 10, id_lists_sync_interval = 60)
|
@@ -11,11 +13,12 @@ module Statsig
|
|
11
13
|
@store = {
|
12
14
|
:gates => {},
|
13
15
|
:configs => {},
|
16
|
+
:layers => {},
|
14
17
|
:id_lists => {},
|
15
18
|
}
|
16
19
|
e = download_config_specs
|
17
20
|
error_callback.call(e) unless error_callback.nil?
|
18
|
-
|
21
|
+
get_id_lists
|
19
22
|
|
20
23
|
@config_sync_thread = sync_config_specs
|
21
24
|
@id_lists_sync_thread = sync_id_lists
|
@@ -34,6 +37,10 @@ module Statsig
|
|
34
37
|
@store[:configs].key?(config_name)
|
35
38
|
end
|
36
39
|
|
40
|
+
def has_layer?(layer_name)
|
41
|
+
@store[:layers].key?(layer_name)
|
42
|
+
end
|
43
|
+
|
37
44
|
def get_gate(gate_name)
|
38
45
|
return nil unless has_gate?(gate_name)
|
39
46
|
@store[:gates][gate_name]
|
@@ -44,6 +51,11 @@ module Statsig
|
|
44
51
|
@store[:configs][config_name]
|
45
52
|
end
|
46
53
|
|
54
|
+
def get_layer(layer_name)
|
55
|
+
return nil unless has_layer?(layer_name)
|
56
|
+
@store[:layers][layer_name]
|
57
|
+
end
|
58
|
+
|
47
59
|
def get_id_list(list_name)
|
48
60
|
@store[:id_lists][list_name]
|
49
61
|
end
|
@@ -63,7 +75,7 @@ module Statsig
|
|
63
75
|
Thread.new do
|
64
76
|
loop do
|
65
77
|
sleep @id_lists_sync_interval
|
66
|
-
|
78
|
+
get_id_lists
|
67
79
|
end
|
68
80
|
end
|
69
81
|
end
|
@@ -89,62 +101,111 @@ module Statsig
|
|
89
101
|
@last_sync_time = specs_json['time'] || @last_sync_time
|
90
102
|
return unless specs_json['has_updates'] == true &&
|
91
103
|
!specs_json['feature_gates'].nil? &&
|
92
|
-
!specs_json['dynamic_configs'].nil?
|
104
|
+
!specs_json['dynamic_configs'].nil? &&
|
105
|
+
!specs_json['layer_configs'].nil?
|
93
106
|
|
94
107
|
new_gates = {}
|
95
108
|
new_configs = {}
|
109
|
+
new_layers = {}
|
96
110
|
|
97
111
|
specs_json['feature_gates'].map{|gate| new_gates[gate['name']] = gate }
|
98
112
|
specs_json['dynamic_configs'].map{|config| new_configs[config['name']] = config }
|
113
|
+
specs_json['layer_configs'].map{|layer| new_layers[layer['name']] = layer }
|
99
114
|
@store[:gates] = new_gates
|
100
115
|
@store[:configs] = new_configs
|
116
|
+
@store[:layers] = new_layers
|
117
|
+
end
|
101
118
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
119
|
+
def get_id_lists
|
120
|
+
response, e = @network.post_helper('get_id_lists', JSON.generate({'statsigMetadata' => Statsig.get_statsig_metadata}))
|
121
|
+
if !e.nil? || response.nil?
|
122
|
+
return
|
123
|
+
end
|
124
|
+
|
125
|
+
begin
|
126
|
+
server_id_lists = JSON.parse(response)
|
127
|
+
local_id_lists = @store[:id_lists]
|
128
|
+
if !server_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
|
129
|
+
return
|
108
130
|
end
|
131
|
+
threads = []
|
109
132
|
|
110
|
-
|
111
|
-
|
112
|
-
|
133
|
+
server_id_lists.each do |list_name, list|
|
134
|
+
server_list = IDList.new(list)
|
135
|
+
local_list = get_id_list(list_name)
|
136
|
+
|
137
|
+
unless local_list.is_a? IDList
|
138
|
+
local_list = IDList.new(list)
|
139
|
+
local_list.size = 0
|
140
|
+
local_id_lists[list_name] = local_list
|
141
|
+
end
|
142
|
+
|
143
|
+
# skip if server list is invalid
|
144
|
+
if server_list.url.nil? || server_list.creation_time < local_list.creation_time || server_list.file_id.nil?
|
145
|
+
next
|
146
|
+
end
|
147
|
+
|
148
|
+
# skip if server list returns a newer file
|
149
|
+
if server_list.file_id != local_list.file_id && server_list.creation_time >= local_list.creation_time
|
150
|
+
local_list = IDList.new(list)
|
151
|
+
local_list.size = 0
|
152
|
+
local_id_lists[list_name] = local_list
|
153
|
+
end
|
154
|
+
|
155
|
+
# skip if server list is no bigger than local list, which means nothing new to read
|
156
|
+
if server_list.size <= local_list.size
|
157
|
+
next
|
158
|
+
end
|
159
|
+
|
160
|
+
threads << Thread.new do
|
161
|
+
download_single_id_list(local_list)
|
113
162
|
end
|
114
163
|
end
|
164
|
+
threads.each(&:join)
|
165
|
+
delete_lists = []
|
166
|
+
local_id_lists.each do |list_name, list|
|
167
|
+
unless server_id_lists.key? list_name
|
168
|
+
delete_lists.push list_name
|
169
|
+
end
|
170
|
+
end
|
171
|
+
delete_lists.each do |list_name|
|
172
|
+
local_id_lists.delete list_name
|
173
|
+
end
|
174
|
+
rescue
|
175
|
+
# Ignored, will try again
|
115
176
|
end
|
116
177
|
end
|
117
178
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
# Ignored
|
143
|
-
end
|
144
|
-
end
|
179
|
+
def download_single_id_list(list)
|
180
|
+
nil unless list.is_a? IDList
|
181
|
+
http = HTTP.headers({'Range' => "bytes=#{list&.size || 0}-"}).accept(:json)
|
182
|
+
begin
|
183
|
+
res = http.get(list.url)
|
184
|
+
nil unless res.status.success?
|
185
|
+
content_length = Integer(res['content-length'])
|
186
|
+
nil if content_length.nil? || content_length <= 0
|
187
|
+
content = res.body.to_s
|
188
|
+
unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
|
189
|
+
@store[:id_lists].delete(list.name)
|
190
|
+
return
|
191
|
+
end
|
192
|
+
ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
|
193
|
+
lines = content.split(/\r?\n/)
|
194
|
+
lines.each do |li|
|
195
|
+
line = li.strip
|
196
|
+
next if line.length <= 1
|
197
|
+
op = line[0]
|
198
|
+
id = line[1..]
|
199
|
+
if op == '+'
|
200
|
+
ids_clone.add(id)
|
201
|
+
elsif op == '-'
|
202
|
+
ids_clone.delete(id)
|
145
203
|
end
|
146
204
|
end
|
147
|
-
|
205
|
+
list.ids = ids_clone
|
206
|
+
list.size = list.size + content_length
|
207
|
+
rescue
|
208
|
+
nil
|
148
209
|
end
|
149
210
|
end
|
150
211
|
end
|
data/lib/statsig.rb
CHANGED
@@ -22,7 +22,12 @@ module Statsig
|
|
22
22
|
|
23
23
|
def self.get_experiment(user, experiment_name)
|
24
24
|
ensure_initialized
|
25
|
-
@shared_instance&.
|
25
|
+
@shared_instance&.get_experiment(user, experiment_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.get_layer(user, layer_name)
|
29
|
+
ensure_initialized
|
30
|
+
@shared_instance&.get_layer(user, layer_name)
|
26
31
|
end
|
27
32
|
|
28
33
|
def self.log_event(user, event_name, value, metadata)
|
@@ -40,7 +45,7 @@ module Statsig
|
|
40
45
|
def self.get_statsig_metadata
|
41
46
|
{
|
42
47
|
'sdkType' => 'ruby-server',
|
43
|
-
'sdkVersion' => '1.
|
48
|
+
'sdkVersion' => '1.9.2',
|
44
49
|
}
|
45
50
|
end
|
46
51
|
|
data/lib/statsig_driver.rb
CHANGED
@@ -6,6 +6,8 @@ require 'statsig_logger'
|
|
6
6
|
require 'statsig_options'
|
7
7
|
require 'statsig_user'
|
8
8
|
require 'spec_store'
|
9
|
+
require 'dynamic_config'
|
10
|
+
require 'layer'
|
9
11
|
|
10
12
|
class StatsigDriver
|
11
13
|
def initialize(secret_key, options = nil, error_callback = nil)
|
@@ -26,12 +28,7 @@ class StatsigDriver
|
|
26
28
|
end
|
27
29
|
|
28
30
|
def check_gate(user, gate_name)
|
29
|
-
|
30
|
-
user = normalize_user(user)
|
31
|
-
if !gate_name.is_a?(String) || gate_name.empty?
|
32
|
-
raise 'Invalid gate_name provided'
|
33
|
-
end
|
34
|
-
check_shutdown
|
31
|
+
user = verify_inputs(user, gate_name, "gate_name")
|
35
32
|
|
36
33
|
res = @evaluator.check_gate(user, gate_name)
|
37
34
|
if res.nil?
|
@@ -49,33 +46,34 @@ class StatsigDriver
|
|
49
46
|
end
|
50
47
|
|
51
48
|
def get_config(user, dynamic_config_name)
|
52
|
-
|
53
|
-
user
|
54
|
-
|
55
|
-
raise "Invalid dynamic_config_name provided"
|
56
|
-
end
|
57
|
-
check_shutdown
|
49
|
+
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
50
|
+
get_config_impl(user, dynamic_config_name)
|
51
|
+
end
|
58
52
|
|
59
|
-
|
53
|
+
def get_experiment(user, experiment_name)
|
54
|
+
user = verify_inputs(user, experiment_name, "experiment_name")
|
55
|
+
get_config_impl(user, experiment_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_layer(user, layer_name)
|
59
|
+
user = verify_inputs(user, layer_name, "layer_name")
|
60
|
+
|
61
|
+
res = @evaluator.get_layer(user, layer_name)
|
60
62
|
if res.nil?
|
61
|
-
res = Statsig::ConfigResult.new(
|
63
|
+
res = Statsig::ConfigResult.new(layer_name)
|
62
64
|
end
|
63
65
|
|
64
66
|
if res == $fetch_from_server
|
65
|
-
res
|
67
|
+
if res.config_delegate.empty?
|
68
|
+
return Layer.new(layer_name)
|
69
|
+
end
|
70
|
+
res = get_config_fallback(user, res.config_delegate)
|
66
71
|
# exposure logged by the server
|
67
|
-
else
|
68
|
-
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures)
|
69
72
|
end
|
70
73
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def get_experiment(user, experiment_name)
|
75
|
-
if !experiment_name.is_a?(String) || experiment_name.empty?
|
76
|
-
raise "Invalid experiment_name provided"
|
77
|
-
end
|
78
|
-
get_config(user, experiment_name)
|
74
|
+
Layer.new(res.name, res.json_value, res.rule_id, lambda { |layer, parameter_name|
|
75
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
76
|
+
})
|
79
77
|
end
|
80
78
|
|
81
79
|
def log_event(user, event_name, value = nil, metadata = nil)
|
@@ -102,6 +100,32 @@ class StatsigDriver
|
|
102
100
|
|
103
101
|
private
|
104
102
|
|
103
|
+
def verify_inputs(user, config_name, variable_name)
|
104
|
+
validate_user(user)
|
105
|
+
if !config_name.is_a?(String) || config_name.empty?
|
106
|
+
raise "Invalid #{variable_name} provided"
|
107
|
+
end
|
108
|
+
|
109
|
+
check_shutdown
|
110
|
+
normalize_user(user)
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_config_impl(user, config_name)
|
114
|
+
res = @evaluator.get_config(user, config_name)
|
115
|
+
if res.nil?
|
116
|
+
res = Statsig::ConfigResult.new(config_name)
|
117
|
+
end
|
118
|
+
|
119
|
+
if res == $fetch_from_server
|
120
|
+
res = get_config_fallback(user, config_name)
|
121
|
+
# exposure logged by the server
|
122
|
+
else
|
123
|
+
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures)
|
124
|
+
end
|
125
|
+
|
126
|
+
DynamicConfig.new(res.name, res.json_value, res.rule_id)
|
127
|
+
end
|
128
|
+
|
105
129
|
def validate_user(user)
|
106
130
|
if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
|
107
131
|
raise 'Must provide a valid StatsigUser with a user_id to use the server SDK. See https://docs.statsig.com/messages/serverRequiredUserID/ for more details.'
|
data/lib/statsig_logger.rb
CHANGED
@@ -2,6 +2,7 @@ require 'statsig_event'
|
|
2
2
|
|
3
3
|
$gate_exposure_event = 'statsig::gate_exposure'
|
4
4
|
$config_exposure_event = 'statsig::config_exposure'
|
5
|
+
$layer_exposure_event = 'statsig::layer_exposure'
|
5
6
|
|
6
7
|
module Statsig
|
7
8
|
class StatsigLogger
|
@@ -43,6 +44,29 @@ module Statsig
|
|
43
44
|
log_event(event)
|
44
45
|
end
|
45
46
|
|
47
|
+
def log_layer_exposure(user, layer, parameter_name, config_evaluation)
|
48
|
+
exposures = config_evaluation.undelegated_sec_exps
|
49
|
+
allocated_experiment = ''
|
50
|
+
is_explicit = (config_evaluation.explicit_parameters&.include? parameter_name) || false
|
51
|
+
if is_explicit
|
52
|
+
allocated_experiment = config_evaluation.config_delegate
|
53
|
+
exposures = config_evaluation.secondary_exposures
|
54
|
+
end
|
55
|
+
|
56
|
+
event = StatsigEvent.new($layer_exposure_event)
|
57
|
+
event.user = user
|
58
|
+
event.metadata = {
|
59
|
+
'config' => layer.name,
|
60
|
+
'ruleID' => layer.rule_id,
|
61
|
+
'allocatedExperiment' => allocated_experiment,
|
62
|
+
'parameterName' => parameter_name,
|
63
|
+
'isExplicitParameter' => String(is_explicit)
|
64
|
+
}
|
65
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
66
|
+
event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
|
67
|
+
log_event(event)
|
68
|
+
end
|
69
|
+
|
46
70
|
def periodic_flush
|
47
71
|
Thread.new do
|
48
72
|
loop do
|
data/lib/statsig_options.rb
CHANGED
@@ -2,7 +2,7 @@ class StatsigOptions
|
|
2
2
|
attr_reader :environment
|
3
3
|
attr_reader :api_url_base
|
4
4
|
|
5
|
-
def initialize(environment = nil, api_url_base = 'https://
|
5
|
+
def initialize(environment = nil, api_url_base = 'https://statsigapi.net/v1')
|
6
6
|
@environment = environment.is_a?(Hash) ? environment : nil
|
7
7
|
@api_url_base = api_url_base
|
8
8
|
end
|
data/lib/statsig_user.rb
CHANGED
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.9.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Statsig, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -84,16 +84,22 @@ dependencies:
|
|
84
84
|
name: http
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '4.4'
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '6.0'
|
90
93
|
type: :runtime
|
91
94
|
prerelease: false
|
92
95
|
version_requirements: !ruby/object:Gem::Requirement
|
93
96
|
requirements:
|
94
|
-
- - "
|
97
|
+
- - ">="
|
95
98
|
- !ruby/object:Gem::Version
|
96
99
|
version: '4.4'
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '6.0'
|
97
103
|
- !ruby/object:Gem::Dependency
|
98
104
|
name: ip3country
|
99
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -118,6 +124,8 @@ files:
|
|
118
124
|
- lib/dynamic_config.rb
|
119
125
|
- lib/evaluation_helpers.rb
|
120
126
|
- lib/evaluator.rb
|
127
|
+
- lib/id_list.rb
|
128
|
+
- lib/layer.rb
|
121
129
|
- lib/network.rb
|
122
130
|
- lib/spec_store.rb
|
123
131
|
- lib/statsig.rb
|