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.
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
- begin
43
- if !@options.data_store.nil?
44
- puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
45
- else
46
- tracker = @diagnostics.track('bootstrap', 'process')
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
- tracker.end(@init_reason == EvaluationReason::BOOTSTRAP)
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 = sync_config_specs
74
- @id_lists_sync_thread = sync_id_lists
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? or !@config_sync_thread.alive?
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? or !@id_lists_sync_thread.alive?
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 sync_config_specs
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
- @diagnostics = Diagnostics.new('config_sync')
163
- loop do
164
- sleep @options.rulesets_sync_interval
165
- if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
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
- end
207
+ })
171
208
  end
172
209
  end
173
210
 
174
- def sync_id_lists
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
- @diagnostics = Diagnostics.new('config_sync')
177
- loop do
178
- sleep @id_lists_sync_interval
179
- if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
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
- end
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
- tracker.end(code)
291
- if !e.nil? || response.nil?
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', nil, { url: list.url })
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', nil, { url: list.url })
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.25.2',
242
+ 'sdkVersion' => '1.27.0',
231
243
  }
232
244
  end
233
245
 
@@ -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('success')
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
- user = verify_inputs(user, gate_name, "gate_name")
56
+ run_with_diagnostics(task: lambda {
57
+ user = verify_inputs(user, gate_name, "gate_name")
58
58
 
59
- res = @evaluator.check_gate(user, gate_name)
60
- if res.nil?
61
- res = Statsig::ConfigResult.new(gate_name)
62
- end
59
+ res = @evaluator.check_gate(user, gate_name)
60
+ if res.nil?
61
+ res = Statsig::ConfigResult.new(gate_name)
62
+ end
63
63
 
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)
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
- res.gate_value
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
- res = @evaluator.check_gate(user, gate_name)
81
- context = {'is_manual_exposure' => true}
82
- @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
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
- user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
94
- get_config_impl(user, dynamic_config_name, options)
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
- user = verify_inputs(user, experiment_name, "experiment_name")
107
- get_config_impl(user, experiment_name, options)
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
- res = @evaluator.get_config(user, config_name)
115
- context = {'is_manual_exposure' => true}
116
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
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
- user = verify_inputs(user, layer_name, "layer_name")
136
+ run_with_diagnostics(task: lambda {
137
+ user = verify_inputs(user, layer_name, "layer_name")
128
138
 
129
- res = @evaluator.get_layer(user, layer_name)
130
- if res.nil?
131
- res = Statsig::ConfigResult.new(layer_name)
132
- end
139
+ res = @evaluator.get_layer(user, layer_name)
140
+ if res.nil?
141
+ res = Statsig::ConfigResult.new(layer_name)
142
+ end
133
143
 
134
- if res == $fetch_from_server
135
- if res.config_delegate.empty?
136
- return Layer.new(layer_name)
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
- res = get_config_fallback(user, res.config_delegate)
139
- # exposure logged by the server
140
- end
141
-
142
- exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
143
- @logger.log_layer_exposure(user, layer, parameter_name, res)
144
- } : nil
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
- res = @evaluator.get_layer(user, layer_name)
155
- layer = Layer.new(layer_name, res.json_value, res.rule_id)
156
- context = {'is_manual_exposure' => true}
157
- @logger.log_layer_exposure(user, layer, parameter_name, res, context)
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)
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Statsig
2
3
  class UninitializedError < StandardError
3
4
  def initialize(msg="Must call initialize first.")