statsig 1.25.1 → 1.26.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/client_initialize_helpers.rb +29 -3
- data/lib/diagnostics.rb +82 -15
- data/lib/error_boundary.rb +41 -34
- data/lib/evaluator.rb +6 -5
- data/lib/hash_utils.rb +17 -0
- data/lib/network.rb +57 -46
- data/lib/spec_store.rb +109 -64
- data/lib/statsig.rb +6 -4
- data/lib/statsig_driver.rb +106 -84
- data/lib/statsig_errors.rb +1 -0
- data/lib/statsig_logger.rb +25 -15
- data/lib/statsig_options.rb +8 -0
- data/lib/ua_parser.rb +1 -0
- data/lib/uri_helper.rb +37 -0
- metadata +52 -2
data/lib/spec_store.rb
CHANGED
@@ -12,7 +12,7 @@ module Statsig
|
|
12
12
|
attr_accessor :initial_config_sync_time
|
13
13
|
attr_accessor :init_reason
|
14
14
|
|
15
|
-
def initialize(network, options, error_callback,
|
15
|
+
def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
|
16
16
|
@init_reason = EvaluationReason::UNINITIALIZED
|
17
17
|
@network = network
|
18
18
|
@options = options
|
@@ -27,8 +27,12 @@ module Statsig
|
|
27
27
|
:configs => {},
|
28
28
|
:layers => {},
|
29
29
|
:id_lists => {},
|
30
|
-
:experiment_to_layer => {}
|
30
|
+
:experiment_to_layer => {},
|
31
|
+
:sdk_keys_to_app_ids => {}
|
31
32
|
}
|
33
|
+
@diagnostics = diagnostics
|
34
|
+
@error_boundary = error_boundary
|
35
|
+
@logger = logger
|
32
36
|
|
33
37
|
@id_list_thread_pool = Concurrent::FixedThreadPool.new(
|
34
38
|
options.idlist_threadpool_size,
|
@@ -38,37 +42,36 @@ module Statsig
|
|
38
42
|
)
|
39
43
|
|
40
44
|
unless @options.bootstrap_values.nil?
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
45
|
+
if !@options.data_store.nil?
|
46
|
+
puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
|
47
|
+
else
|
48
|
+
tracker = @diagnostics.track('bootstrap', 'process')
|
49
|
+
begin
|
46
50
|
if process_specs(options.bootstrap_values)
|
47
51
|
@init_reason = EvaluationReason::BOOTSTRAP
|
48
52
|
end
|
49
|
-
|
53
|
+
rescue
|
54
|
+
puts 'the provided bootstrapValues is not a valid JSON string'
|
55
|
+
ensure
|
56
|
+
tracker.end(success: @init_reason == EvaluationReason::BOOTSTRAP)
|
50
57
|
end
|
51
|
-
rescue
|
52
|
-
puts 'the provided bootstrapValues is not a valid JSON string'
|
53
58
|
end
|
54
59
|
end
|
55
60
|
|
56
61
|
unless @options.data_store.nil?
|
57
|
-
init_diagnostics&.mark("data_store", "start", "load")
|
58
62
|
@options.data_store.init
|
59
|
-
load_config_specs_from_storage_adapter
|
60
|
-
init_diagnostics&.mark("data_store", "end", "load", @init_reason == EvaluationReason::DATA_ADAPTER)
|
63
|
+
load_config_specs_from_storage_adapter
|
61
64
|
end
|
62
65
|
|
63
66
|
if @init_reason == EvaluationReason::UNINITIALIZED
|
64
|
-
download_config_specs
|
67
|
+
download_config_specs
|
65
68
|
end
|
66
69
|
|
67
70
|
@initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
|
68
71
|
if !@options.data_store.nil?
|
69
|
-
get_id_lists_from_adapter
|
72
|
+
get_id_lists_from_adapter
|
70
73
|
else
|
71
|
-
get_id_lists_from_network
|
74
|
+
get_id_lists_from_network
|
72
75
|
end
|
73
76
|
|
74
77
|
@config_sync_thread = sync_config_specs
|
@@ -120,35 +123,47 @@ module Statsig
|
|
120
123
|
@specs[:id_lists][list_name]
|
121
124
|
end
|
122
125
|
|
126
|
+
def has_sdk_key?(sdk_key)
|
127
|
+
@specs[:sdk_keys_to_app_ids].key?(sdk_key)
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_app_id_for_sdk_key(sdk_key)
|
131
|
+
if sdk_key.nil?
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
return nil unless has_sdk_key?(sdk_key)
|
135
|
+
@specs[:sdk_keys_to_app_ids][sdk_key]
|
136
|
+
end
|
137
|
+
|
123
138
|
def get_raw_specs
|
124
139
|
@specs
|
125
140
|
end
|
126
141
|
|
127
142
|
def maybe_restart_background_threads
|
128
|
-
if @config_sync_thread.nil?
|
143
|
+
if @config_sync_thread.nil? || !@config_sync_thread.alive?
|
129
144
|
@config_sync_thread = sync_config_specs
|
130
145
|
end
|
131
|
-
if @id_lists_sync_thread.nil?
|
146
|
+
if @id_lists_sync_thread.nil? || !@id_lists_sync_thread.alive?
|
132
147
|
@id_lists_sync_thread = sync_id_lists
|
133
148
|
end
|
134
149
|
end
|
135
150
|
|
136
151
|
private
|
137
152
|
|
138
|
-
def load_config_specs_from_storage_adapter
|
139
|
-
|
153
|
+
def load_config_specs_from_storage_adapter
|
154
|
+
tracker = @diagnostics.track('data_store_config_specs', 'fetch')
|
140
155
|
cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
141
|
-
|
156
|
+
tracker.end(success: true)
|
142
157
|
return if cached_values.nil?
|
143
158
|
|
144
|
-
|
159
|
+
tracker = @diagnostics.track('data_store_config_specs', 'process')
|
145
160
|
process_specs(cached_values, from_adapter: true)
|
146
161
|
@init_reason = EvaluationReason::DATA_ADAPTER
|
147
|
-
|
162
|
+
tracker.end(success: true)
|
148
163
|
rescue StandardError
|
149
164
|
# Fallback to network
|
150
|
-
|
151
|
-
download_config_specs
|
165
|
+
tracker.end(success: false)
|
166
|
+
download_config_specs
|
152
167
|
end
|
153
168
|
|
154
169
|
def save_config_specs_to_storage_adapter(specs_string)
|
@@ -160,32 +175,40 @@ module Statsig
|
|
160
175
|
|
161
176
|
def sync_config_specs
|
162
177
|
Thread.new do
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
178
|
+
@error_boundary.capture(task: lambda {
|
179
|
+
loop do
|
180
|
+
@diagnostics = Diagnostics.new('config_sync')
|
181
|
+
sleep @options.rulesets_sync_interval
|
182
|
+
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
183
|
+
load_config_specs_from_storage_adapter
|
184
|
+
else
|
185
|
+
download_config_specs
|
186
|
+
end
|
187
|
+
@logger.log_diagnostics_event(@diagnostics)
|
169
188
|
end
|
170
|
-
|
189
|
+
})
|
171
190
|
end
|
172
191
|
end
|
173
192
|
|
174
193
|
def sync_id_lists
|
175
194
|
Thread.new do
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
195
|
+
@error_boundary.capture(task: lambda {
|
196
|
+
loop do
|
197
|
+
@diagnostics = Diagnostics.new('config_sync')
|
198
|
+
sleep @id_lists_sync_interval
|
199
|
+
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
|
200
|
+
get_id_lists_from_adapter
|
201
|
+
else
|
202
|
+
get_id_lists_from_network
|
203
|
+
end
|
204
|
+
@logger.log_diagnostics_event(@diagnostics)
|
182
205
|
end
|
183
|
-
|
206
|
+
})
|
184
207
|
end
|
185
208
|
end
|
186
209
|
|
187
|
-
def download_config_specs
|
188
|
-
|
210
|
+
def download_config_specs
|
211
|
+
tracker = @diagnostics.track('download_config_specs', 'network_request')
|
189
212
|
|
190
213
|
error = nil
|
191
214
|
begin
|
@@ -194,18 +217,17 @@ module Statsig
|
|
194
217
|
if e.is_a? NetworkError
|
195
218
|
code = e.http_code
|
196
219
|
end
|
197
|
-
|
220
|
+
tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
198
221
|
|
199
222
|
if e.nil?
|
200
223
|
unless response.nil?
|
201
|
-
|
202
|
-
|
224
|
+
tracker = @diagnostics.track('download_config_specs', 'process')
|
203
225
|
if process_specs(response.body.to_s)
|
204
226
|
@init_reason = EvaluationReason::NETWORK
|
205
|
-
@rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
|
206
227
|
end
|
228
|
+
tracker.end(success: @init_reason == EvaluationReason::NETWORK)
|
207
229
|
|
208
|
-
|
230
|
+
@rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
|
209
231
|
end
|
210
232
|
|
211
233
|
nil
|
@@ -252,6 +274,9 @@ module Statsig
|
|
252
274
|
@specs[:configs] = new_configs
|
253
275
|
@specs[:layers] = new_layers
|
254
276
|
@specs[:experiment_to_layer] = new_exp_to_layer
|
277
|
+
@specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
|
278
|
+
|
279
|
+
specs_json['diagnostics']
|
255
280
|
|
256
281
|
unless from_adapter
|
257
282
|
save_config_specs_to_storage_adapter(specs_string)
|
@@ -259,18 +284,18 @@ module Statsig
|
|
259
284
|
true
|
260
285
|
end
|
261
286
|
|
262
|
-
def get_id_lists_from_adapter
|
263
|
-
|
287
|
+
def get_id_lists_from_adapter
|
288
|
+
tracker = @diagnostics.track('data_store_id_lists', 'fetch')
|
264
289
|
cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
|
265
290
|
return if cached_values.nil?
|
266
291
|
|
267
|
-
|
292
|
+
tracker.end(success: true)
|
268
293
|
id_lists = JSON.parse(cached_values)
|
269
|
-
process_id_lists(id_lists,
|
294
|
+
process_id_lists(id_lists, from_adapter: true)
|
270
295
|
rescue StandardError
|
271
296
|
# Fallback to network
|
272
|
-
|
273
|
-
get_id_lists_from_network
|
297
|
+
tracker.end(success: false)
|
298
|
+
get_id_lists_from_network
|
274
299
|
end
|
275
300
|
|
276
301
|
def save_id_lists_to_adapter(id_lists_raw_json)
|
@@ -280,36 +305,46 @@ module Statsig
|
|
280
305
|
@options.data_store.set(Interfaces::IDataStore::ID_LISTS_KEY, id_lists_raw_json)
|
281
306
|
end
|
282
307
|
|
283
|
-
def get_id_lists_from_network
|
284
|
-
|
308
|
+
def get_id_lists_from_network
|
309
|
+
tracker = @diagnostics.track('get_id_list_sources', 'network_request')
|
285
310
|
response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
|
286
|
-
|
311
|
+
code = response&.status.to_i
|
312
|
+
if e.is_a? NetworkError
|
313
|
+
code = e.http_code
|
314
|
+
end
|
315
|
+
success = e.nil? && !response.nil?
|
316
|
+
tracker.end(statusCode: code, success: success, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
317
|
+
if !success
|
287
318
|
return
|
288
319
|
end
|
289
|
-
init_diagnostics&.mark("get_id_lists", "end", "network_request", response.status.to_i)
|
290
320
|
|
291
321
|
begin
|
292
322
|
server_id_lists = JSON.parse(response)
|
293
|
-
process_id_lists(server_id_lists
|
323
|
+
process_id_lists(server_id_lists)
|
294
324
|
save_id_lists_to_adapter(response.body.to_s)
|
295
325
|
rescue
|
296
326
|
# Ignored, will try again
|
297
327
|
end
|
298
328
|
end
|
299
329
|
|
300
|
-
def process_id_lists(new_id_lists,
|
330
|
+
def process_id_lists(new_id_lists, from_adapter: false)
|
301
331
|
local_id_lists = @specs[:id_lists]
|
302
332
|
if !new_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
|
303
333
|
return
|
304
334
|
end
|
305
335
|
tasks = []
|
306
336
|
|
307
|
-
|
337
|
+
tracker = @diagnostics.track(
|
338
|
+
from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
|
339
|
+
'process',
|
340
|
+
{ idListCount: new_id_lists.length }
|
341
|
+
)
|
342
|
+
|
343
|
+
if new_id_lists.empty?
|
344
|
+
tracker.end(success: true)
|
308
345
|
return
|
309
346
|
end
|
310
347
|
|
311
|
-
init_diagnostics&.mark("get_id_lists", "start", "process", new_id_lists.length)
|
312
|
-
|
313
348
|
delete_lists = []
|
314
349
|
local_id_lists.each do |list_name, list|
|
315
350
|
unless new_id_lists.key? list_name
|
@@ -357,14 +392,17 @@ module Statsig
|
|
357
392
|
end
|
358
393
|
|
359
394
|
result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
|
360
|
-
|
395
|
+
tracker.end(success: result.state == :fulfilled)
|
361
396
|
end
|
362
397
|
|
363
398
|
def get_single_id_list_from_adapter(list)
|
399
|
+
tracker = @diagnostics.track('data_store_id_list', 'fetch', { url: list.url })
|
364
400
|
cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
|
401
|
+
tracker.end(success: true)
|
365
402
|
content = cached_values.to_s
|
366
|
-
process_single_id_list(list, content)
|
403
|
+
process_single_id_list(list, content, from_adapter: true)
|
367
404
|
rescue StandardError
|
405
|
+
tracker.end(success: false)
|
368
406
|
nil
|
369
407
|
end
|
370
408
|
|
@@ -377,8 +415,10 @@ module Statsig
|
|
377
415
|
def download_single_id_list(list)
|
378
416
|
nil unless list.is_a? IDList
|
379
417
|
http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
|
418
|
+
tracker = @diagnostics.track('get_id_list', 'network_request', { url: list.url })
|
380
419
|
begin
|
381
420
|
res = http.get(list.url)
|
421
|
+
tracker.end(statusCode: res.status.code, success: res.status.success?)
|
382
422
|
nil unless res.status.success?
|
383
423
|
content_length = Integer(res['content-length'])
|
384
424
|
nil if content_length.nil? || content_length <= 0
|
@@ -386,15 +426,18 @@ module Statsig
|
|
386
426
|
success = process_single_id_list(list, content, content_length)
|
387
427
|
save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
|
388
428
|
rescue
|
429
|
+
tracker.end(success: false)
|
389
430
|
nil
|
390
431
|
end
|
391
432
|
end
|
392
433
|
|
393
|
-
def process_single_id_list(list, content, content_length = nil)
|
434
|
+
def process_single_id_list(list, content, content_length = nil, from_adapter: false)
|
394
435
|
false unless list.is_a? IDList
|
395
436
|
begin
|
437
|
+
tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
|
396
438
|
unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
|
397
439
|
@specs[:id_lists].delete(list.name)
|
440
|
+
tracker.end(success: false)
|
398
441
|
return false
|
399
442
|
end
|
400
443
|
ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
|
@@ -416,8 +459,10 @@ module Statsig
|
|
416
459
|
else
|
417
460
|
list.size + content_length
|
418
461
|
end
|
462
|
+
tracker.end(success: true)
|
419
463
|
return true
|
420
464
|
rescue
|
465
|
+
tracker.end(success: false)
|
421
466
|
return false
|
422
467
|
end
|
423
468
|
end
|
data/lib/statsig.rb
CHANGED
@@ -208,17 +208,19 @@ module Statsig
|
|
208
208
|
@shared_instance&.override_config(config_name, config_value)
|
209
209
|
end
|
210
210
|
|
211
|
-
sig { params(user: StatsigUser).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
211
|
+
sig { params(user: StatsigUser, hash: String, client_sdk_key: T.any(String, NilClass)).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
212
212
|
##
|
213
213
|
# Gets all evaluated values for the given user.
|
214
214
|
# These values can then be given to a Statsig Client SDK via bootstrapping.
|
215
215
|
#
|
216
216
|
# @param user A StatsigUser object used for the evaluation
|
217
|
+
# @param hash The type of hashing algorithm to use ('sha256', 'djb2', 'none')
|
218
|
+
# @param client_sdk_key A optional client sdk key to be used for the evaluation
|
217
219
|
#
|
218
220
|
# @note See Ruby Documentation: https://docs.statsig.com/server/rubySDK)
|
219
|
-
def self.get_client_initialize_response(user)
|
221
|
+
def self.get_client_initialize_response(user, hash: 'sha256', client_sdk_key: nil)
|
220
222
|
ensure_initialized
|
221
|
-
@shared_instance&.get_client_initialize_response(user)
|
223
|
+
@shared_instance&.get_client_initialize_response(user, hash, client_sdk_key)
|
222
224
|
end
|
223
225
|
|
224
226
|
sig { returns(T::Hash[String, String]) }
|
@@ -227,7 +229,7 @@ module Statsig
|
|
227
229
|
def self.get_statsig_metadata
|
228
230
|
{
|
229
231
|
'sdkType' => 'ruby-server',
|
230
|
-
'sdkVersion' => '1.
|
232
|
+
'sdkVersion' => '1.26.0',
|
231
233
|
}
|
232
234
|
end
|
233
235
|
|