statsig 1.25.2 → 1.27.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 +51 -24
- data/lib/error_boundary.rb +30 -44
- data/lib/evaluator.rb +6 -5
- data/lib/hash_utils.rb +17 -0
- data/lib/network.rb +54 -43
- data/lib/spec_store.rb +97 -53
- data/lib/statsig.rb +16 -4
- data/lib/statsig_driver.rb +101 -59
- data/lib/statsig_errors.rb +1 -0
- data/lib/statsig_logger.rb +26 -17
- data/lib/statsig_options.rb +14 -0
- data/lib/ua_parser.rb +1 -0
- metadata +55 -6
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, diagnostics)
|
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,9 +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
|
}
|
32
33
|
@diagnostics = diagnostics
|
34
|
+
@error_boundary = error_boundary
|
35
|
+
@logger = logger
|
33
36
|
|
34
37
|
@id_list_thread_pool = Concurrent::FixedThreadPool.new(
|
35
38
|
options.idlist_threadpool_size,
|
@@ -39,18 +42,19 @@ module Statsig
|
|
39
42
|
)
|
40
43
|
|
41
44
|
unless @options.bootstrap_values.nil?
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
47
50
|
if process_specs(options.bootstrap_values)
|
48
51
|
@init_reason = EvaluationReason::BOOTSTRAP
|
49
52
|
end
|
50
|
-
|
53
|
+
rescue
|
54
|
+
puts 'the provided bootstrapValues is not a valid JSON string'
|
55
|
+
ensure
|
56
|
+
tracker.end(success: @init_reason == EvaluationReason::BOOTSTRAP)
|
51
57
|
end
|
52
|
-
rescue
|
53
|
-
puts 'the provided bootstrapValues is not a valid JSON string'
|
54
58
|
end
|
55
59
|
end
|
56
60
|
|
@@ -70,8 +74,8 @@ module Statsig
|
|
70
74
|
get_id_lists_from_network
|
71
75
|
end
|
72
76
|
|
73
|
-
@config_sync_thread =
|
74
|
-
@id_lists_sync_thread =
|
77
|
+
@config_sync_thread = spawn_sync_config_specs_thread
|
78
|
+
@id_lists_sync_thread = spawn_sync_id_lists_thread
|
75
79
|
end
|
76
80
|
|
77
81
|
def is_ready_for_checks
|
@@ -119,34 +123,66 @@ module Statsig
|
|
119
123
|
@specs[:id_lists][list_name]
|
120
124
|
end
|
121
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
|
+
|
122
138
|
def get_raw_specs
|
123
139
|
@specs
|
124
140
|
end
|
125
141
|
|
126
142
|
def maybe_restart_background_threads
|
127
|
-
if @config_sync_thread.nil?
|
143
|
+
if @config_sync_thread.nil? || !@config_sync_thread.alive?
|
128
144
|
@config_sync_thread = sync_config_specs
|
129
145
|
end
|
130
|
-
if @id_lists_sync_thread.nil?
|
146
|
+
if @id_lists_sync_thread.nil? || !@id_lists_sync_thread.alive?
|
131
147
|
@id_lists_sync_thread = sync_id_lists
|
132
148
|
end
|
133
149
|
end
|
134
150
|
|
151
|
+
def sync_config_specs
|
152
|
+
@diagnostics.context = 'config_sync'
|
153
|
+
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
154
|
+
load_config_specs_from_storage_adapter
|
155
|
+
else
|
156
|
+
download_config_specs
|
157
|
+
end
|
158
|
+
@logger.log_diagnostics_event(@diagnostics)
|
159
|
+
end
|
160
|
+
|
161
|
+
def sync_id_lists
|
162
|
+
@diagnostics.context = 'config_sync'
|
163
|
+
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
|
164
|
+
get_id_lists_from_adapter
|
165
|
+
else
|
166
|
+
get_id_lists_from_network
|
167
|
+
end
|
168
|
+
@logger.log_diagnostics_event(@diagnostics)
|
169
|
+
end
|
170
|
+
|
135
171
|
private
|
136
172
|
|
137
173
|
def load_config_specs_from_storage_adapter
|
138
174
|
tracker = @diagnostics.track('data_store_config_specs', 'fetch')
|
139
175
|
cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
140
|
-
tracker.end(true)
|
176
|
+
tracker.end(success: true)
|
141
177
|
return if cached_values.nil?
|
142
178
|
|
143
179
|
tracker = @diagnostics.track('data_store_config_specs', 'process')
|
144
180
|
process_specs(cached_values, from_adapter: true)
|
145
181
|
@init_reason = EvaluationReason::DATA_ADAPTER
|
146
|
-
tracker.end(true)
|
182
|
+
tracker.end(success: true)
|
147
183
|
rescue StandardError
|
148
184
|
# Fallback to network
|
149
|
-
tracker.end(false)
|
185
|
+
tracker.end(success: false)
|
150
186
|
download_config_specs
|
151
187
|
end
|
152
188
|
|
@@ -157,31 +193,33 @@ module Statsig
|
|
157
193
|
@options.data_store.set(Interfaces::IDataStore::CONFIG_SPECS_KEY, specs_string)
|
158
194
|
end
|
159
195
|
|
160
|
-
def
|
196
|
+
def spawn_sync_config_specs_thread
|
197
|
+
if @options.disable_rulesets_sync
|
198
|
+
return nil
|
199
|
+
end
|
200
|
+
|
161
201
|
Thread.new do
|
162
|
-
@
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
load_config_specs_from_storage_adapter
|
167
|
-
else
|
168
|
-
download_config_specs
|
202
|
+
@error_boundary.capture(task: lambda {
|
203
|
+
loop do
|
204
|
+
sleep @options.rulesets_sync_interval
|
205
|
+
sync_config_specs
|
169
206
|
end
|
170
|
-
|
207
|
+
})
|
171
208
|
end
|
172
209
|
end
|
173
210
|
|
174
|
-
def
|
211
|
+
def spawn_sync_id_lists_thread
|
212
|
+
if @options.disable_idlists_sync
|
213
|
+
return nil
|
214
|
+
end
|
215
|
+
|
175
216
|
Thread.new do
|
176
|
-
@
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
get_id_lists_from_adapter
|
181
|
-
else
|
182
|
-
get_id_lists_from_network
|
217
|
+
@error_boundary.capture(task: lambda {
|
218
|
+
loop do
|
219
|
+
sleep @id_lists_sync_interval
|
220
|
+
sync_id_lists
|
183
221
|
end
|
184
|
-
|
222
|
+
})
|
185
223
|
end
|
186
224
|
end
|
187
225
|
|
@@ -195,15 +233,16 @@ module Statsig
|
|
195
233
|
if e.is_a? NetworkError
|
196
234
|
code = e.http_code
|
197
235
|
end
|
198
|
-
tracker.end(code)
|
236
|
+
tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
199
237
|
|
200
238
|
if e.nil?
|
201
239
|
unless response.nil?
|
202
240
|
tracker = @diagnostics.track('download_config_specs', 'process')
|
241
|
+
|
203
242
|
if process_specs(response.body.to_s)
|
204
243
|
@init_reason = EvaluationReason::NETWORK
|
205
244
|
end
|
206
|
-
tracker.end(@init_reason == EvaluationReason::NETWORK)
|
245
|
+
tracker.end(success: @init_reason == EvaluationReason::NETWORK)
|
207
246
|
|
208
247
|
@rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
|
209
248
|
end
|
@@ -241,6 +280,7 @@ module Statsig
|
|
241
280
|
specs_json['feature_gates'].each { |gate| new_gates[gate['name']] = gate }
|
242
281
|
specs_json['dynamic_configs'].each { |config| new_configs[config['name']] = config }
|
243
282
|
specs_json['layer_configs'].each { |layer| new_layers[layer['name']] = layer }
|
283
|
+
specs_json['diagnostics']&.each { |key, value| @diagnostics.sample_rates[key] = value }
|
244
284
|
|
245
285
|
if specs_json['layers'].is_a?(Hash)
|
246
286
|
specs_json['layers'].each { |layer_name, experiments|
|
@@ -252,6 +292,7 @@ module Statsig
|
|
252
292
|
@specs[:configs] = new_configs
|
253
293
|
@specs[:layers] = new_layers
|
254
294
|
@specs[:experiment_to_layer] = new_exp_to_layer
|
295
|
+
@specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
|
255
296
|
|
256
297
|
unless from_adapter
|
257
298
|
save_config_specs_to_storage_adapter(specs_string)
|
@@ -264,12 +305,12 @@ module Statsig
|
|
264
305
|
cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
|
265
306
|
return if cached_values.nil?
|
266
307
|
|
267
|
-
tracker.end(true)
|
308
|
+
tracker.end(success: true)
|
268
309
|
id_lists = JSON.parse(cached_values)
|
269
310
|
process_id_lists(id_lists, from_adapter: true)
|
270
311
|
rescue StandardError
|
271
312
|
# Fallback to network
|
272
|
-
tracker.end(false)
|
313
|
+
tracker.end(success: false)
|
273
314
|
get_id_lists_from_network
|
274
315
|
end
|
275
316
|
|
@@ -287,8 +328,9 @@ module Statsig
|
|
287
328
|
if e.is_a? NetworkError
|
288
329
|
code = e.http_code
|
289
330
|
end
|
290
|
-
|
291
|
-
|
331
|
+
success = e.nil? && !response.nil?
|
332
|
+
tracker.end(statusCode: code, success: success, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
333
|
+
if !success
|
292
334
|
return
|
293
335
|
end
|
294
336
|
|
@@ -311,11 +353,11 @@ module Statsig
|
|
311
353
|
tracker = @diagnostics.track(
|
312
354
|
from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
|
313
355
|
'process',
|
314
|
-
new_id_lists.length
|
356
|
+
{ idListCount: new_id_lists.length }
|
315
357
|
)
|
316
358
|
|
317
359
|
if new_id_lists.empty?
|
318
|
-
tracker.end
|
360
|
+
tracker.end(success: true)
|
319
361
|
return
|
320
362
|
end
|
321
363
|
|
@@ -366,17 +408,17 @@ module Statsig
|
|
366
408
|
end
|
367
409
|
|
368
410
|
result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
|
369
|
-
tracker.end(result.state == :fulfilled)
|
411
|
+
tracker.end(success: result.state == :fulfilled)
|
370
412
|
end
|
371
413
|
|
372
414
|
def get_single_id_list_from_adapter(list)
|
373
|
-
tracker = @diagnostics.track('data_store_id_list', 'fetch',
|
415
|
+
tracker = @diagnostics.track('data_store_id_list', 'fetch', { url: list.url })
|
374
416
|
cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
|
375
|
-
tracker.end(true)
|
417
|
+
tracker.end(success: true)
|
376
418
|
content = cached_values.to_s
|
377
419
|
process_single_id_list(list, content, from_adapter: true)
|
378
420
|
rescue StandardError
|
379
|
-
tracker.end(false)
|
421
|
+
tracker.end(success: false)
|
380
422
|
nil
|
381
423
|
end
|
382
424
|
|
@@ -389,10 +431,10 @@ module Statsig
|
|
389
431
|
def download_single_id_list(list)
|
390
432
|
nil unless list.is_a? IDList
|
391
433
|
http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
|
434
|
+
tracker = @diagnostics.track('get_id_list', 'network_request', { url: list.url })
|
392
435
|
begin
|
393
|
-
tracker = @diagnostics.track('get_id_list', 'network_request', nil, { url: list.url })
|
394
436
|
res = http.get(list.url)
|
395
|
-
tracker.end(res.status.code)
|
437
|
+
tracker.end(statusCode: res.status.code, success: res.status.success?)
|
396
438
|
nil unless res.status.success?
|
397
439
|
content_length = Integer(res['content-length'])
|
398
440
|
nil if content_length.nil? || content_length <= 0
|
@@ -400,6 +442,7 @@ module Statsig
|
|
400
442
|
success = process_single_id_list(list, content, content_length)
|
401
443
|
save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
|
402
444
|
rescue
|
445
|
+
tracker.end(success: false)
|
403
446
|
nil
|
404
447
|
end
|
405
448
|
end
|
@@ -407,10 +450,10 @@ module Statsig
|
|
407
450
|
def process_single_id_list(list, content, content_length = nil, from_adapter: false)
|
408
451
|
false unless list.is_a? IDList
|
409
452
|
begin
|
410
|
-
tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process',
|
453
|
+
tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
|
411
454
|
unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
|
412
455
|
@specs[:id_lists].delete(list.name)
|
413
|
-
tracker.end(false)
|
456
|
+
tracker.end(success: false)
|
414
457
|
return false
|
415
458
|
end
|
416
459
|
ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
|
@@ -432,12 +475,13 @@ module Statsig
|
|
432
475
|
else
|
433
476
|
list.size + content_length
|
434
477
|
end
|
435
|
-
tracker.end(true)
|
478
|
+
tracker.end(success: true)
|
436
479
|
return true
|
437
480
|
rescue
|
438
|
-
tracker.end(false)
|
481
|
+
tracker.end(success: false)
|
439
482
|
return false
|
440
483
|
end
|
441
484
|
end
|
485
|
+
|
442
486
|
end
|
443
487
|
end
|
data/lib/statsig.rb
CHANGED
@@ -176,6 +176,16 @@ module Statsig
|
|
176
176
|
@shared_instance&.log_event(user, event_name, value, metadata)
|
177
177
|
end
|
178
178
|
|
179
|
+
def self.sync_rulesets
|
180
|
+
ensure_initialized
|
181
|
+
@shared_instance&.manually_sync_rulesets
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.sync_idlists
|
185
|
+
ensure_initialized
|
186
|
+
@shared_instance&.manually_sync_idlists
|
187
|
+
end
|
188
|
+
|
179
189
|
sig { void }
|
180
190
|
##
|
181
191
|
# Stops all Statsig activity and flushes any pending events.
|
@@ -208,17 +218,19 @@ module Statsig
|
|
208
218
|
@shared_instance&.override_config(config_name, config_value)
|
209
219
|
end
|
210
220
|
|
211
|
-
sig { params(user: StatsigUser).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
221
|
+
sig { params(user: StatsigUser, hash: String, client_sdk_key: T.any(String, NilClass)).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
212
222
|
##
|
213
223
|
# Gets all evaluated values for the given user.
|
214
224
|
# These values can then be given to a Statsig Client SDK via bootstrapping.
|
215
225
|
#
|
216
226
|
# @param user A StatsigUser object used for the evaluation
|
227
|
+
# @param hash The type of hashing algorithm to use ('sha256', 'djb2', 'none')
|
228
|
+
# @param client_sdk_key A optional client sdk key to be used for the evaluation
|
217
229
|
#
|
218
230
|
# @note See Ruby Documentation: https://docs.statsig.com/server/rubySDK)
|
219
|
-
def self.get_client_initialize_response(user)
|
231
|
+
def self.get_client_initialize_response(user, hash: 'sha256', client_sdk_key: nil)
|
220
232
|
ensure_initialized
|
221
|
-
@shared_instance&.get_client_initialize_response(user)
|
233
|
+
@shared_instance&.get_client_initialize_response(user, hash, client_sdk_key)
|
222
234
|
end
|
223
235
|
|
224
236
|
sig { returns(T::Hash[String, String]) }
|
@@ -227,7 +239,7 @@ module Statsig
|
|
227
239
|
def self.get_statsig_metadata
|
228
240
|
{
|
229
241
|
'sdkType' => 'ruby-server',
|
230
|
-
'sdkVersion' => '1.
|
242
|
+
'sdkVersion' => '1.27.0',
|
231
243
|
}
|
232
244
|
end
|
233
245
|
|
data/lib/statsig_driver.rb
CHANGED
@@ -37,13 +37,12 @@ class StatsigDriver
|
|
37
37
|
@shutdown = false
|
38
38
|
@secret_key = secret_key
|
39
39
|
@net = Statsig::Network.new(secret_key, @options)
|
40
|
-
@logger = Statsig::StatsigLogger.new(@net, @options)
|
41
|
-
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics)
|
42
|
-
tracker.end(
|
40
|
+
@logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
|
41
|
+
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger)
|
42
|
+
tracker.end(success: true)
|
43
43
|
|
44
44
|
@logger.log_diagnostics_event(@diagnostics)
|
45
|
-
})
|
46
|
-
@err_boundary.logger = @logger
|
45
|
+
}, caller: __method__.to_s)
|
47
46
|
end
|
48
47
|
|
49
48
|
class CheckGateOptions < T::Struct
|
@@ -54,32 +53,36 @@ class StatsigDriver
|
|
54
53
|
|
55
54
|
def check_gate(user, gate_name, options = CheckGateOptions.new)
|
56
55
|
@err_boundary.capture(task: lambda {
|
57
|
-
|
56
|
+
run_with_diagnostics(task: lambda {
|
57
|
+
user = verify_inputs(user, gate_name, "gate_name")
|
58
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
res = @evaluator.check_gate(user, gate_name)
|
60
|
+
if res.nil?
|
61
|
+
res = Statsig::ConfigResult.new(gate_name)
|
62
|
+
end
|
63
63
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
64
|
+
if res == $fetch_from_server
|
65
|
+
res = check_gate_fallback(user, gate_name)
|
66
|
+
# exposure logged by the server
|
67
|
+
else
|
68
|
+
if options.log_exposure
|
69
|
+
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
70
|
+
end
|
70
71
|
end
|
71
|
-
end
|
72
72
|
|
73
|
-
|
73
|
+
res.gate_value
|
74
|
+
}, caller: __method__.to_s)
|
74
75
|
}, recover: -> { false }, caller: __method__.to_s)
|
75
76
|
end
|
76
77
|
|
77
78
|
sig { params(user: StatsigUser, gate_name: String).void }
|
78
79
|
|
79
80
|
def manually_log_gate_exposure(user, gate_name)
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
@err_boundary.capture(task: lambda {
|
82
|
+
res = @evaluator.check_gate(user, gate_name)
|
83
|
+
context = { 'is_manual_exposure' => true }
|
84
|
+
@logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
85
|
+
})
|
83
86
|
end
|
84
87
|
|
85
88
|
class GetConfigOptions < T::Struct
|
@@ -90,8 +93,10 @@ class StatsigDriver
|
|
90
93
|
|
91
94
|
def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
|
92
95
|
@err_boundary.capture(task: lambda {
|
93
|
-
|
94
|
-
|
96
|
+
run_with_diagnostics(task: lambda {
|
97
|
+
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
98
|
+
get_config_impl(user, dynamic_config_name, options)
|
99
|
+
}, caller: __method__.to_s)
|
95
100
|
}, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
|
96
101
|
end
|
97
102
|
|
@@ -103,17 +108,21 @@ class StatsigDriver
|
|
103
108
|
|
104
109
|
def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
|
105
110
|
@err_boundary.capture(task: lambda {
|
106
|
-
|
107
|
-
|
111
|
+
run_with_diagnostics(task: lambda {
|
112
|
+
user = verify_inputs(user, experiment_name, "experiment_name")
|
113
|
+
get_config_impl(user, experiment_name, options)
|
114
|
+
}, caller: __method__.to_s)
|
108
115
|
}, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
|
109
116
|
end
|
110
117
|
|
111
118
|
sig { params(user: StatsigUser, config_name: String).void }
|
112
119
|
|
113
120
|
def manually_log_config_exposure(user, config_name)
|
114
|
-
|
115
|
-
|
116
|
-
|
121
|
+
@err_boundary.capture(task: lambda {
|
122
|
+
res = @evaluator.get_config(user, config_name)
|
123
|
+
context = { 'is_manual_exposure' => true }
|
124
|
+
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
125
|
+
}, caller: __method__.to_s)
|
117
126
|
end
|
118
127
|
|
119
128
|
class GetLayerOptions < T::Struct
|
@@ -124,37 +133,39 @@ class StatsigDriver
|
|
124
133
|
|
125
134
|
def get_layer(user, layer_name, options = GetLayerOptions.new)
|
126
135
|
@err_boundary.capture(task: lambda {
|
127
|
-
|
136
|
+
run_with_diagnostics(task: lambda {
|
137
|
+
user = verify_inputs(user, layer_name, "layer_name")
|
128
138
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
139
|
+
res = @evaluator.get_layer(user, layer_name)
|
140
|
+
if res.nil?
|
141
|
+
res = Statsig::ConfigResult.new(layer_name)
|
142
|
+
end
|
133
143
|
|
134
|
-
|
135
|
-
|
136
|
-
|
144
|
+
if res == $fetch_from_server
|
145
|
+
if res.config_delegate.empty?
|
146
|
+
return Layer.new(layer_name)
|
147
|
+
end
|
148
|
+
res = get_config_fallback(user, res.config_delegate)
|
149
|
+
# exposure logged by the server
|
137
150
|
end
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
146
|
-
}, recover: lambda {
|
147
|
-
Layer.new(layer_name)
|
148
|
-
}, caller: __method__.to_s)
|
151
|
+
|
152
|
+
exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
|
153
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
154
|
+
} : nil
|
155
|
+
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
156
|
+
}, caller: __method__.to_s)
|
157
|
+
}, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
|
149
158
|
end
|
150
159
|
|
151
160
|
sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
|
152
161
|
|
153
162
|
def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
163
|
+
@err_boundary.capture(task: lambda {
|
164
|
+
res = @evaluator.get_layer(user, layer_name)
|
165
|
+
layer = Layer.new(layer_name, res.json_value, res.rule_id)
|
166
|
+
context = { 'is_manual_exposure' => true }
|
167
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res, context)
|
168
|
+
}, caller: __method__.to_s)
|
158
169
|
end
|
159
170
|
|
160
171
|
def log_event(user, event_name, value = nil, metadata = nil)
|
@@ -171,7 +182,19 @@ class StatsigDriver
|
|
171
182
|
event.value = value
|
172
183
|
event.metadata = metadata
|
173
184
|
@logger.log_event(event)
|
174
|
-
})
|
185
|
+
}, caller: __method__.to_s)
|
186
|
+
end
|
187
|
+
|
188
|
+
def manually_sync_rulesets
|
189
|
+
@err_boundary.capture(task: lambda {
|
190
|
+
@evaluator.spec_store.sync_config_specs
|
191
|
+
}, caller: __method__.to_s)
|
192
|
+
end
|
193
|
+
|
194
|
+
def manually_sync_idlists
|
195
|
+
@err_boundary.capture(task: lambda {
|
196
|
+
@evaluator.spec_store.sync_id_lists
|
197
|
+
}, caller: __method__.to_s)
|
175
198
|
end
|
176
199
|
|
177
200
|
def shutdown
|
@@ -179,29 +202,30 @@ class StatsigDriver
|
|
179
202
|
@shutdown = true
|
180
203
|
@logger.shutdown
|
181
204
|
@evaluator.shutdown
|
182
|
-
})
|
205
|
+
}, caller: __method__.to_s)
|
183
206
|
end
|
184
207
|
|
185
208
|
def override_gate(gate_name, gate_value)
|
186
209
|
@err_boundary.capture(task: lambda {
|
187
210
|
@evaluator.override_gate(gate_name, gate_value)
|
188
|
-
})
|
211
|
+
}, caller: __method__.to_s)
|
189
212
|
end
|
190
213
|
|
191
214
|
def override_config(config_name, config_value)
|
192
215
|
@err_boundary.capture(task: lambda {
|
193
216
|
@evaluator.override_config(config_name, config_value)
|
194
|
-
})
|
217
|
+
}, caller: __method__.to_s)
|
195
218
|
end
|
196
219
|
|
197
220
|
# @param [StatsigUser] user
|
221
|
+
# @param [String | nil] client_sdk_key
|
198
222
|
# @return [Hash]
|
199
|
-
def get_client_initialize_response(user)
|
223
|
+
def get_client_initialize_response(user, hash, client_sdk_key)
|
200
224
|
@err_boundary.capture(task: lambda {
|
201
225
|
validate_user(user)
|
202
226
|
normalize_user(user)
|
203
|
-
@evaluator.get_client_initialize_response(user)
|
204
|
-
}, recover: -> { nil })
|
227
|
+
@evaluator.get_client_initialize_response(user, hash, client_sdk_key)
|
228
|
+
}, recover: -> { nil }, caller: __method__.to_s)
|
205
229
|
end
|
206
230
|
|
207
231
|
def maybe_restart_background_threads
|
@@ -212,11 +236,29 @@ class StatsigDriver
|
|
212
236
|
@err_boundary.capture(task: lambda {
|
213
237
|
@evaluator.maybe_restart_background_threads
|
214
238
|
@logger.maybe_restart_background_threads
|
215
|
-
})
|
239
|
+
}, caller: __method__.to_s)
|
216
240
|
end
|
217
241
|
|
218
242
|
private
|
219
243
|
|
244
|
+
def run_with_diagnostics(task:, caller:)
|
245
|
+
diagnostics = nil
|
246
|
+
if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(1)
|
247
|
+
diagnostics = Statsig::Diagnostics.new('api_call')
|
248
|
+
tracker = diagnostics.track(caller)
|
249
|
+
end
|
250
|
+
begin
|
251
|
+
res = task.call
|
252
|
+
tracker&.end(success: true)
|
253
|
+
rescue StandardError => e
|
254
|
+
tracker&.end(success: false)
|
255
|
+
raise e
|
256
|
+
ensure
|
257
|
+
@logger.log_diagnostics_event(diagnostics)
|
258
|
+
end
|
259
|
+
return res
|
260
|
+
end
|
261
|
+
|
220
262
|
sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
|
221
263
|
|
222
264
|
def verify_inputs(user, config_name, variable_name)
|
data/lib/statsig_errors.rb
CHANGED