statsig 2.1.0 → 2.5.5

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
@@ -19,8 +19,9 @@ module Statsig
19
19
  attr_accessor :sdk_keys_to_app_ids
20
20
  attr_accessor :hashed_sdk_keys_to_app_ids
21
21
  attr_accessor :unsupported_configs
22
+ attr_accessor :cmab_configs
22
23
 
23
- def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key)
24
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key, sdk_config)
24
25
  @init_reason = EvaluationReason::UNINITIALIZED
25
26
  @network = network
26
27
  @options = options
@@ -33,6 +34,7 @@ module Statsig
33
34
  @gates = {}
34
35
  @configs = {}
35
36
  @layers = {}
37
+ @cmab_configs = {}
36
38
  @condition_map = {}
37
39
  @id_lists = {}
38
40
  @experiment_to_layer = {}
@@ -43,6 +45,9 @@ module Statsig
43
45
  @logger = logger
44
46
  @secret_key = secret_key
45
47
  @unsupported_configs = Set.new
48
+ @sdk_configs = sdk_config
49
+
50
+ startTime = (Time.now.to_f * 1000).to_i
46
51
 
47
52
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
48
53
  options.idlist_threadpool_size,
@@ -57,7 +62,7 @@ module Statsig
57
62
  else
58
63
  tracker = @diagnostics.track('initialize','bootstrap', 'process')
59
64
  begin
60
- if process_specs(options.bootstrap_values)
65
+ if process_specs(options.bootstrap_values).nil?
61
66
  @init_reason = EvaluationReason::BOOTSTRAP
62
67
  end
63
68
  rescue StandardError
@@ -68,13 +73,15 @@ module Statsig
68
73
  end
69
74
  end
70
75
 
76
+ failure_details = nil
77
+
71
78
  unless @options.data_store.nil?
72
79
  @options.data_store.init
73
- load_config_specs_from_storage_adapter('initialize')
80
+ failure_details = load_config_specs_from_storage_adapter('initialize')
74
81
  end
75
82
 
76
83
  if @init_reason == EvaluationReason::UNINITIALIZED
77
- download_config_specs('initialize')
84
+ failure_details = download_config_specs('initialize')
78
85
  end
79
86
 
80
87
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
@@ -86,12 +93,18 @@ module Statsig
86
93
 
87
94
  @config_sync_thread = spawn_sync_config_specs_thread
88
95
  @id_lists_sync_thread = spawn_sync_id_lists_thread
96
+ endTime = (Time.now.to_f * 1000).to_i
97
+ @initialization_details = {duration: endTime - startTime, isSDKReady: true, configSpecReady: @init_reason != EvaluationReason::UNINITIALIZED, failureDetails: failure_details}
89
98
  end
90
99
 
91
100
  def is_ready_for_checks
92
101
  @last_config_sync_time != 0
93
102
  end
94
103
 
104
+ def get_initialization_details
105
+ @initialization_details
106
+ end
107
+
95
108
  def shutdown
96
109
  @config_sync_thread&.exit
97
110
  @id_lists_sync_thread&.exit
@@ -114,6 +127,13 @@ module Statsig
114
127
  @layers.key?(layer_name.to_sym)
115
128
  end
116
129
 
130
+ def has_cmab_config?(config_name)
131
+ if @cmab_configs.nil?
132
+ return false
133
+ end
134
+ @cmab_configs.key?(config_name.to_sym)
135
+ end
136
+
117
137
  def get_gate(gate_name)
118
138
  gate_sym = gate_name.to_sym
119
139
  return nil unless has_gate?(gate_sym)
@@ -134,6 +154,12 @@ module Statsig
134
154
  @layers[layer_sym]
135
155
  end
136
156
 
157
+ def get_cmab_config(config_name)
158
+ config_sym = config_name.to_sym
159
+ return nil unless has_cmab_config?(config_sym)
160
+ @cmab_configs[config_sym]
161
+ end
162
+
137
163
  def get_condition(condition_hash)
138
164
  @condition_map[condition_hash.to_sym]
139
165
  end
@@ -202,13 +228,19 @@ module Statsig
202
228
  return if cached_values.nil?
203
229
 
204
230
  tracker = @diagnostics.track(context, 'data_store_config_specs', 'process')
205
- process_specs(cached_values, from_adapter: true)
206
- @init_reason = EvaluationReason::DATA_ADAPTER
207
- tracker.end(success: true)
231
+ failure_details = process_specs(cached_values, from_adapter: true)
232
+ if failure_details.nil?
233
+ @init_reason = EvaluationReason::DATA_ADAPTER
234
+ tracker.end(success: true)
235
+ else
236
+ tracker.end(success: false)
237
+ return download_config_specs(context)
238
+ end
239
+ return failure_details
208
240
  rescue StandardError
209
241
  # Fallback to network
210
242
  tracker.end(success: false)
211
- download_config_specs(context)
243
+ return download_config_specs(context)
212
244
  end
213
245
 
214
246
  def save_rulesets_to_storage_adapter(rulesets_string)
@@ -253,18 +285,21 @@ module Statsig
253
285
  tracker = @diagnostics.track(context, 'download_config_specs', 'network_request')
254
286
 
255
287
  error = nil
288
+ failure_details = nil
256
289
  begin
257
290
  response, e = @network.download_config_specs(@last_config_sync_time)
258
291
  code = response&.status.to_i
259
292
  if e.is_a? NetworkError
260
293
  code = e.http_code
294
+ failure_details = {statusCode: code, exception: e, reason: "CONFIG_SPECS_NETWORK_ERROR"}
261
295
  end
262
296
  tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
263
297
 
264
298
  if e.nil?
265
299
  unless response.nil?
266
300
  tracker = @diagnostics.track(context, 'download_config_specs', 'process')
267
- if process_specs(response.body.to_s)
301
+ failure_details = process_specs(response.body.to_s)
302
+ if failure_details.nil?
268
303
  @init_reason = EvaluationReason::NETWORK
269
304
  end
270
305
  tracker.end(success: @init_reason == EvaluationReason::NETWORK)
@@ -274,59 +309,66 @@ module Statsig
274
309
  @last_config_sync_time)
275
310
  end
276
311
  end
277
-
278
- nil
279
312
  else
280
313
  error = e
281
314
  end
282
315
  rescue StandardError => e
316
+ failure_details = {exception: e, reason: "INTERNAL_ERROR"}
283
317
  error = e
284
318
  end
285
319
 
286
320
  @error_callback.call(error) unless error.nil? or @error_callback.nil?
321
+ return failure_details
287
322
  end
288
323
 
289
324
  def process_specs(specs_string, from_adapter: false)
290
325
  if specs_string.nil?
291
- return false
326
+ return {reason: "EMPTY_SPEC"}
292
327
  end
293
328
 
294
- specs_json = JSON.parse(specs_string, { symbolize_names: true })
295
- return false unless specs_json.is_a? Hash
329
+ begin
330
+ specs_json = JSON.parse(specs_string, { symbolize_names: true })
331
+ return {reason: "PARSE_RESPONSE_ERROR"} unless specs_json.is_a? Hash
296
332
 
297
- hashed_sdk_key_used = specs_json[:hashed_sdk_key_used]
298
- unless hashed_sdk_key_used.nil? or hashed_sdk_key_used == Statsig::HashUtils.djb2(@secret_key)
299
- err_boundary.log_exception(Statsig::InvalidSDKKeyResponse.new)
300
- return false
301
- end
333
+ hashed_sdk_key_used = specs_json[:hashed_sdk_key_used]
334
+ unless hashed_sdk_key_used.nil? or hashed_sdk_key_used == Statsig::HashUtils.djb2(@secret_key)
335
+ err_boundary.log_exception(Statsig::InvalidSDKKeyResponse.new)
336
+ return {reason: "PARSE_RESPONSE_ERROR"}
337
+ end
302
338
 
303
- new_specs_sync_time = specs_json[:time]
304
- if new_specs_sync_time.nil? \
305
- || new_specs_sync_time < @last_config_sync_time \
306
- || specs_json[:has_updates] != true \
307
- || specs_json[:feature_gates].nil? \
308
- || specs_json[:dynamic_configs].nil? \
309
- || specs_json[:layer_configs].nil?
310
- return false
311
- end
339
+ new_specs_sync_time = specs_json[:time]
340
+ if new_specs_sync_time.nil? \
341
+ || new_specs_sync_time < @last_config_sync_time \
342
+ || specs_json[:has_updates] != true \
343
+ || specs_json[:feature_gates].nil? \
344
+ || specs_json[:dynamic_configs].nil? \
345
+ || specs_json[:layer_configs].nil?
346
+ return {reason: "PARSE_RESPONSE_ERROR"}
347
+ end
312
348
 
313
- @last_config_sync_time = new_specs_sync_time
314
- @unsupported_configs.clear
349
+ @last_config_sync_time = new_specs_sync_time
350
+ @unsupported_configs.clear
315
351
 
316
- specs_json[:diagnostics]&.each { |key, value| @diagnostics.sample_rates[key.to_s] = value }
352
+ specs_json[:diagnostics]&.each { |key, value| @diagnostics.sample_rates[key.to_s] = value }
317
353
 
318
- @gates = specs_json[:feature_gates]
319
- @configs = specs_json[:dynamic_configs]
320
- @layers = specs_json[:layer_configs]
321
- @condition_map = specs_json[:condition_map]
322
- @experiment_to_layer = specs_json[:experiment_to_layer]
323
- @sdk_keys_to_app_ids = specs_json[:sdk_keys_to_app_ids] || {}
324
- @hashed_sdk_keys_to_app_ids = specs_json[:hashed_sdk_keys_to_app_ids] || {}
354
+ @gates = specs_json[:feature_gates]
355
+ @configs = specs_json[:dynamic_configs]
356
+ @layers = specs_json[:layer_configs]
357
+ @cmab_configs = specs_json[:cmab_configs]
358
+ @condition_map = specs_json[:condition_map]
359
+ @experiment_to_layer = specs_json[:experiment_to_layer]
360
+ @sdk_keys_to_app_ids = specs_json[:sdk_keys_to_app_ids] || {}
361
+ @hashed_sdk_keys_to_app_ids = specs_json[:hashed_sdk_keys_to_app_ids] || {}
362
+ @sdk_configs.set_flags(specs_json[:sdk_flags])
363
+ @sdk_configs.set_configs(specs_json[:sdk_configs])
325
364
 
326
- unless from_adapter
327
- save_rulesets_to_storage_adapter(specs_string)
365
+ unless from_adapter
366
+ save_rulesets_to_storage_adapter(specs_string)
367
+ end
368
+ rescue StandardError => e
369
+ return {reason: "PARSE_RESPONSE_ERROR"}
328
370
  end
329
- true
371
+ nil
330
372
  end
331
373
 
332
374
  def get_id_lists_from_adapter(context)
data/lib/statsig.rb CHANGED
@@ -20,6 +20,13 @@ module Statsig
20
20
  @shared_instance = StatsigDriver.new(secret_key, options, error_callback)
21
21
  end
22
22
 
23
+ def self.get_initialization_details
24
+ if not defined? @shared_instance or @shared_instance.nil?
25
+ return {duration: 0, isSDKReady: false, configSpecReady: false, failure_details: {exception: Statsig::UninitializedError.new, reason: 'INTERNAL_ERROR'}}
26
+ end
27
+ @shared_instance.get_initialization_details
28
+ end
29
+
23
30
  class GetGateOptions
24
31
  attr_accessor :disable_log_exposure, :skip_evaluation, :disable_evaluation_details
25
32
 
@@ -65,7 +72,7 @@ module Statsig
65
72
  end
66
73
 
67
74
  ##
68
- # @deprecated - use check_gate(user, gate, options) and disable_exposure_logging in options
75
+ # @deprecated - use check_gate(user, gate, options) with CheckGateOptions.new(disable_log_exposure: true) as options
69
76
  # Gets the boolean result of a gate, evaluated against the given user.
70
77
  #
71
78
  # @param user A StatsigUser object used for the evaluation
@@ -108,7 +115,7 @@ module Statsig
108
115
  end
109
116
 
110
117
  ##
111
- # @deprecated - use get_config(user, config, options) and disable_exposure_logging in options
118
+ # @deprecated - use get_config(user, config, options) with GetConfigOptions.new(disable_log_exposure: true) as options
112
119
  # Get the values of a dynamic config, evaluated against the given user.
113
120
  #
114
121
  # @param [StatsigUser] user A StatsigUser object used for the evaluation
@@ -152,7 +159,7 @@ module Statsig
152
159
  end
153
160
 
154
161
  ##
155
- # @deprecated - use get_experiment(user, experiment, options) and disable_exposure_logging in options
162
+ # @deprecated - use get_experiment(user, experiment, options) with GetExperimentOptions.new(disable_log_exposure: true) as options
156
163
  # Get the values of an experiment, evaluated against the given user.
157
164
  #
158
165
  # @param [StatsigUser] user A StatsigUser object used for the evaluation
@@ -199,7 +206,7 @@ module Statsig
199
206
  end
200
207
 
201
208
  ##
202
- # @deprecated - use get_layer(user, gate, options) and disable_exposure_logging in options
209
+ # @deprecated - use get_layer(user, gate, options) with GetLayerOptions.new(disable_log_exposure: true) as options
203
210
  # Get the values of a layer, evaluated against the given user.
204
211
  #
205
212
  # @param user A StatsigUser object used for the evaluation
@@ -321,6 +328,17 @@ module Statsig
321
328
  @shared_instance&.override_config(config_name, config_value)
322
329
  end
323
330
 
331
+
332
+ ##
333
+ # Overrides an experiment to return the value for a specific group name.
334
+ #
335
+ # @param experiment_name The name of the experiment to be overridden
336
+ # @param group_name The name of the group whose value should be returned
337
+ def self.override_experiment_by_group_name(experiment_name, group_name)
338
+ ensure_initialized
339
+ @shared_instance&.override_experiment_by_group_name(experiment_name, group_name)
340
+ end
341
+
324
342
  def self.remove_config_override(config_name)
325
343
  ensure_initialized
326
344
  @shared_instance&.remove_config_override(config_name)
@@ -331,6 +349,11 @@ module Statsig
331
349
  @shared_instance&.clear_config_overrides
332
350
  end
333
351
 
352
+ def self.clear_experiment_overrides
353
+ ensure_initialized
354
+ @shared_instance&.clear_experiment_overrides
355
+ end
356
+
334
357
  ##
335
358
  # @param [HashTable] debug information log with exposure events
336
359
  def self.set_debug_info(debug_info)
@@ -363,11 +386,15 @@ module Statsig
363
386
  def self.get_statsig_metadata
364
387
  {
365
388
  'sdkType' => 'ruby-server',
366
- 'sdkVersion' => '2.1.0',
389
+ 'sdkVersion' => '2.5.5',
367
390
  'languageVersion' => RUBY_VERSION
368
391
  }
369
392
  end
370
393
 
394
+ def self.get_options
395
+ @driver&.instance_variable_get(:@options)
396
+ end
397
+
371
398
  private
372
399
 
373
400
  def self.ensure_initialized
@@ -13,6 +13,7 @@ require 'error_boundary'
13
13
  require 'layer'
14
14
  require 'memo'
15
15
  require 'diagnostics'
16
+ require 'sdk_configs'
16
17
 
17
18
  class StatsigDriver
18
19
 
@@ -27,15 +28,16 @@ class StatsigDriver
27
28
 
28
29
  @err_boundary = Statsig::ErrorBoundary.new(secret_key, !options.nil? && options.local_mode)
29
30
  @err_boundary.capture(caller: __method__) do
30
- @diagnostics = Statsig::Diagnostics.new()
31
+ @diagnostics = Statsig::Diagnostics.new
32
+ @sdk_configs = Statsig::SDKConfigs.new
31
33
  tracker = @diagnostics.track('initialize', 'overall')
32
34
  @options = options || StatsigOptions.new
33
35
  @shutdown = false
34
36
  @secret_key = secret_key
35
37
  @net = Statsig::Network.new(secret_key, @options)
36
- @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
38
+ @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary, @sdk_configs)
37
39
  @persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
38
- @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key)
40
+ @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key, @sdk_configs)
39
41
  @evaluator = Statsig::Evaluator.new(@store, @options, @persistent_storage_utils)
40
42
  tracker.end(success: true)
41
43
 
@@ -43,6 +45,10 @@ class StatsigDriver
43
45
  end
44
46
  end
45
47
 
48
+ def get_initialization_details
49
+ @store.get_initialization_details
50
+ end
51
+
46
52
  def get_gate_impl(
47
53
  user,
48
54
  gate_name,
@@ -59,7 +65,7 @@ class StatsigDriver
59
65
 
60
66
  user = verify_inputs(user, gate_name, 'gate_name')
61
67
 
62
- Statsig::Memo.for(user.get_memo, :get_gate_impl, gate_name) do
68
+ Statsig::Memo.for(user.get_memo, :get_gate_impl, gate_name, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
63
69
  res = Statsig::ConfigResult.new(
64
70
  name: gate_name,
65
71
  disable_exposures: disable_log_exposure,
@@ -68,9 +74,7 @@ class StatsigDriver
68
74
  @evaluator.check_gate(user, gate_name, res, ignore_local_overrides: ignore_local_overrides)
69
75
 
70
76
  unless disable_log_exposure
71
- @logger.log_gate_exposure(
72
- user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
73
- )
77
+ @logger.log_gate_exposure(user, res)
74
78
  end
75
79
 
76
80
  FeatureGate.from_config_result(res)
@@ -109,7 +113,7 @@ class StatsigDriver
109
113
  res = Statsig::ConfigResult.new(name: gate_name)
110
114
  @evaluator.check_gate(user, gate_name, res)
111
115
  context = { :is_manual_exposure => true }
112
- @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
116
+ @logger.log_gate_exposure(user, res, context)
113
117
  end
114
118
  end
115
119
 
@@ -150,7 +154,7 @@ class StatsigDriver
150
154
  @evaluator.get_config(user, config_name, res)
151
155
 
152
156
  context = { :is_manual_exposure => true }
153
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
157
+ @logger.log_config_exposure(user, res, context)
154
158
  end
155
159
  end
156
160
 
@@ -167,7 +171,7 @@ class StatsigDriver
167
171
  @err_boundary.capture(caller: __method__, recover: -> { Layer.new(layer_name) }) do
168
172
  run_with_diagnostics(caller: :get_layer) do
169
173
  user = verify_inputs(user, layer_name, "layer_name")
170
- Statsig::Memo.for(user.get_memo, :get_layer, layer_name) do
174
+ Statsig::Memo.for(user.get_memo, :get_layer, layer_name, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
171
175
  exposures_disabled = options&.disable_log_exposure == true
172
176
  res = Statsig::ConfigResult.new(
173
177
  name: layer_name,
@@ -300,6 +304,12 @@ class StatsigDriver
300
304
  end
301
305
  end
302
306
 
307
+ def clear_experiment_overrides
308
+ @err_boundary.capture(caller: __method__) do
309
+ @evaluator.clear_experiment_overrides
310
+ end
311
+ end
312
+
303
313
  def set_debug_info(debug_info)
304
314
  @err_boundary.capture(caller: __method__) do
305
315
  @logger.set_debug_info(debug_info)
@@ -333,6 +343,10 @@ class StatsigDriver
333
343
  end
334
344
  end
335
345
 
346
+ def override_experiment_by_group_name(experiment_name, group_name)
347
+ @evaluator.override_experiment_by_group_name(experiment_name, group_name)
348
+ end
349
+
336
350
  private
337
351
 
338
352
  def run_with_diagnostics(caller:)
@@ -357,7 +371,7 @@ class StatsigDriver
357
371
 
358
372
  def verify_inputs(user, config_name, variable_name)
359
373
  validate_user(user)
360
- user = Statsig::Memo.for(user.get_memo(), :verify_inputs, 0) do
374
+ user = Statsig::Memo.for(user.get_memo(), :verify_inputs, 0, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
361
375
  user = normalize_user(user)
362
376
  check_shutdown
363
377
  maybe_restart_background_threads
@@ -372,7 +386,7 @@ class StatsigDriver
372
386
  end
373
387
 
374
388
  def get_config_impl(user, config_name, disable_log_exposure, user_persisted_values: nil, disable_evaluation_details: false, ignore_local_overrides: false)
375
- Statsig::Memo.for(user.get_memo, :get_config_impl, config_name) do
389
+ Statsig::Memo.for(user.get_memo, :get_config_impl, config_name, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
376
390
  res = Statsig::ConfigResult.new(
377
391
  name: config_name,
378
392
  disable_exposures: disable_log_exposure,
@@ -381,7 +395,7 @@ class StatsigDriver
381
395
  @evaluator.get_config(user, config_name, res, user_persisted_values: user_persisted_values, ignore_local_overrides: ignore_local_overrides)
382
396
 
383
397
  unless disable_log_exposure
384
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
398
+ @logger.log_config_exposure(user, res)
385
399
  end
386
400
 
387
401
  DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type, res.evaluation_details)
data/lib/statsig_event.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  class StatsigEvent
3
2
  attr_accessor :value, :metadata, :statsig_metadata, :secondary_exposures
4
3
  attr_reader :user
@@ -15,7 +14,7 @@ class StatsigEvent
15
14
 
16
15
  def user=(value)
17
16
  if value.is_a?(StatsigUser)
18
- @user = Statsig::Memo.for(value.get_memo(), :serialize, 0) do
17
+ @user = Statsig::Memo.for(value.get_memo(), :serialize, 0, disable_evaluation_memoization: Statsig.get_options&.disable_evaluation_memoization) do
19
18
  value.serialize(true)
20
19
  end
21
20
  end