statsig 2.1.0 → 2.8.1

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.
@@ -1,18 +1,20 @@
1
- require 'config_result'
2
- require 'evaluator'
3
- require 'network'
4
- require 'statsig_errors'
5
- require 'statsig_event'
6
- require 'statsig_logger'
7
- require 'statsig_options'
8
- require 'statsig_user'
9
- require 'spec_store'
10
- require 'dynamic_config'
11
- require 'feature_gate'
12
- require 'error_boundary'
13
- require 'layer'
14
- require 'memo'
15
- require 'diagnostics'
1
+ require_relative 'api_config'
2
+ require_relative 'config_result'
3
+ require_relative 'diagnostics'
4
+ require_relative 'dynamic_config'
5
+ require_relative 'error_boundary'
6
+ require_relative 'evaluator'
7
+ require_relative 'feature_gate'
8
+ require_relative 'layer'
9
+ require_relative 'memo'
10
+ require_relative 'network'
11
+ require_relative 'sdk_configs'
12
+ require_relative 'spec_store'
13
+ require_relative 'statsig_errors'
14
+ require_relative 'statsig_event'
15
+ require_relative 'statsig_logger'
16
+ require_relative 'statsig_options'
17
+ require_relative 'statsig_user'
16
18
 
17
19
  class StatsigDriver
18
20
 
@@ -27,15 +29,16 @@ class StatsigDriver
27
29
 
28
30
  @err_boundary = Statsig::ErrorBoundary.new(secret_key, !options.nil? && options.local_mode)
29
31
  @err_boundary.capture(caller: __method__) do
30
- @diagnostics = Statsig::Diagnostics.new()
32
+ @diagnostics = Statsig::Diagnostics.new
33
+ @sdk_configs = Statsig::SDKConfigs.new
31
34
  tracker = @diagnostics.track('initialize', 'overall')
32
35
  @options = options || StatsigOptions.new
33
36
  @shutdown = false
34
37
  @secret_key = secret_key
35
38
  @net = Statsig::Network.new(secret_key, @options)
36
- @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
39
+ @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary, @sdk_configs)
37
40
  @persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
38
- @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key)
41
+ @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key, @sdk_configs)
39
42
  @evaluator = Statsig::Evaluator.new(@store, @options, @persistent_storage_utils)
40
43
  tracker.end(success: true)
41
44
 
@@ -43,6 +46,10 @@ class StatsigDriver
43
46
  end
44
47
  end
45
48
 
49
+ def get_initialization_details
50
+ @store.get_initialization_details
51
+ end
52
+
46
53
  def get_gate_impl(
47
54
  user,
48
55
  gate_name,
@@ -59,7 +66,7 @@ class StatsigDriver
59
66
 
60
67
  user = verify_inputs(user, gate_name, 'gate_name')
61
68
 
62
- Statsig::Memo.for(user.get_memo, :get_gate_impl, gate_name) do
69
+ Statsig::Memo.for(user.get_memo, :get_gate_impl, gate_name, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
63
70
  res = Statsig::ConfigResult.new(
64
71
  name: gate_name,
65
72
  disable_exposures: disable_log_exposure,
@@ -68,9 +75,7 @@ class StatsigDriver
68
75
  @evaluator.check_gate(user, gate_name, res, ignore_local_overrides: ignore_local_overrides)
69
76
 
70
77
  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
- )
78
+ @logger.log_gate_exposure(user, res)
74
79
  end
75
80
 
76
81
  FeatureGate.from_config_result(res)
@@ -109,7 +114,16 @@ class StatsigDriver
109
114
  res = Statsig::ConfigResult.new(name: gate_name)
110
115
  @evaluator.check_gate(user, gate_name, res)
111
116
  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)
117
+ @logger.log_gate_exposure(user, res, context)
118
+ end
119
+ end
120
+
121
+ def get_fields_used_for_gate(gate_name)
122
+ @err_boundary.capture(caller: __method__, recover: -> { [] }) do
123
+ gate = @store.get_gate(gate_name)
124
+ return [] if gate.nil?
125
+
126
+ gate[:fieldsUsed] || []
113
127
  end
114
128
  end
115
129
 
@@ -128,6 +142,15 @@ class StatsigDriver
128
142
  end
129
143
  end
130
144
 
145
+ def get_fields_used_for_config(config_name)
146
+ @err_boundary.capture(caller: __method__, recover: -> { [] }) do
147
+ config = @store.get_config(config_name)
148
+ return [] if config.nil?
149
+
150
+ config[:fieldsUsed] || []
151
+ end
152
+ end
153
+
131
154
  def get_experiment(user, experiment_name, options = nil)
132
155
  @err_boundary.capture(caller: __method__, recover: -> { DynamicConfig.new(experiment_name) }) do
133
156
  run_with_diagnostics(caller: :get_experiment) do
@@ -150,7 +173,7 @@ class StatsigDriver
150
173
  @evaluator.get_config(user, config_name, res)
151
174
 
152
175
  context = { :is_manual_exposure => true }
153
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
176
+ @logger.log_config_exposure(user, res, context)
154
177
  end
155
178
  end
156
179
 
@@ -167,7 +190,7 @@ class StatsigDriver
167
190
  @err_boundary.capture(caller: __method__, recover: -> { Layer.new(layer_name) }) do
168
191
  run_with_diagnostics(caller: :get_layer) do
169
192
  user = verify_inputs(user, layer_name, "layer_name")
170
- Statsig::Memo.for(user.get_memo, :get_layer, layer_name) do
193
+ Statsig::Memo.for(user.get_memo, :get_layer, layer_name, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
171
194
  exposures_disabled = options&.disable_log_exposure == true
172
195
  res = Statsig::ConfigResult.new(
173
196
  name: layer_name,
@@ -197,6 +220,15 @@ class StatsigDriver
197
220
  end
198
221
  end
199
222
 
223
+ def get_fields_used_for_layer(layer_name)
224
+ @err_boundary.capture(caller: __method__, recover: -> { [] }) do
225
+ layer = @store.get_layer(layer_name)
226
+ return [] if layer.nil?
227
+
228
+ layer[:fieldsUsed] || []
229
+ end
230
+ end
231
+
200
232
  def log_event(user, event_name, value = nil, metadata = nil)
201
233
  @err_boundary.capture(caller: __method__) do
202
234
  if !user.nil? && !user.instance_of?(StatsigUser)
@@ -300,6 +332,18 @@ class StatsigDriver
300
332
  end
301
333
  end
302
334
 
335
+ def clear_experiment_overrides
336
+ @err_boundary.capture(caller: __method__) do
337
+ @evaluator.clear_experiment_overrides
338
+ end
339
+ end
340
+
341
+ def remove_experiment_override(experiment_name)
342
+ @err_boundary.capture(caller: __method__) do
343
+ @evaluator.remove_experiment_override(experiment_name)
344
+ end
345
+ end
346
+
303
347
  def set_debug_info(debug_info)
304
348
  @err_boundary.capture(caller: __method__) do
305
349
  @logger.set_debug_info(debug_info)
@@ -333,6 +377,10 @@ class StatsigDriver
333
377
  end
334
378
  end
335
379
 
380
+ def override_experiment_by_group_name(experiment_name, group_name)
381
+ @evaluator.override_experiment_by_group_name(experiment_name, group_name)
382
+ end
383
+
336
384
  private
337
385
 
338
386
  def run_with_diagnostics(caller:)
@@ -357,7 +405,7 @@ class StatsigDriver
357
405
 
358
406
  def verify_inputs(user, config_name, variable_name)
359
407
  validate_user(user)
360
- user = Statsig::Memo.for(user.get_memo(), :verify_inputs, 0) do
408
+ user = Statsig::Memo.for(user.get_memo(), :verify_inputs, 0, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
361
409
  user = normalize_user(user)
362
410
  check_shutdown
363
411
  maybe_restart_background_threads
@@ -372,7 +420,7 @@ class StatsigDriver
372
420
  end
373
421
 
374
422
  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
423
+ Statsig::Memo.for(user.get_memo, :get_config_impl, config_name, disable_evaluation_memoization: @options.disable_evaluation_memoization) do
376
424
  res = Statsig::ConfigResult.new(
377
425
  name: config_name,
378
426
  disable_exposures: disable_log_exposure,
@@ -381,7 +429,7 @@ class StatsigDriver
381
429
  @evaluator.get_config(user, config_name, res, user_persisted_values: user_persisted_values, ignore_local_overrides: ignore_local_overrides)
382
430
 
383
431
  unless disable_log_exposure
384
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
432
+ @logger.log_config_exposure(user, res)
385
433
  end
386
434
 
387
435
  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
@@ -1,15 +1,24 @@
1
- require 'constants'
2
- require 'statsig_event'
3
1
  require 'concurrent-ruby'
2
+ require_relative 'constants'
3
+ require_relative 'hash_utils'
4
+ require_relative 'statsig_event'
5
+ require_relative 'ttl_set'
4
6
 
5
7
  $gate_exposure_event = 'statsig::gate_exposure'
6
8
  $config_exposure_event = 'statsig::config_exposure'
7
9
  $layer_exposure_event = 'statsig::layer_exposure'
8
10
  $diagnostics_event = 'statsig::diagnostics'
9
11
  $ignored_metadata_keys = [:serverTime, :configSyncTime, :initTime, :reason]
12
+
13
+ class EntityType
14
+ GATE = "gate"
15
+ CONFIG = "config"
16
+ LAYER = "layer"
17
+ end
18
+
10
19
  module Statsig
11
20
  class StatsigLogger
12
- def initialize(network, options, error_boundary)
21
+ def initialize(network, options, error_boundary, sdk_configs)
13
22
  @network = network
14
23
  @events = []
15
24
  @options = options
@@ -25,10 +34,12 @@ module Statsig
25
34
 
26
35
  @error_boundary = error_boundary
27
36
  @background_flush = periodic_flush
28
- @deduper = Concurrent::Set.new()
37
+ @deduper = Concurrent::Set.new
38
+ @sampling_key_set = Statsig::TTLSet.new
29
39
  @interval = 0
30
40
  @flush_mutex = Mutex.new
31
41
  @debug_info = nil
42
+ @sdk_configs = sdk_configs
32
43
  end
33
44
 
34
45
  def log_event(event)
@@ -38,43 +49,64 @@ module Statsig
38
49
  end
39
50
  end
40
51
 
41
- def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures, eval_details, context = nil)
52
+ def log_gate_exposure(user, result, context = nil)
53
+ should_log, logged_sampling_rate, shadow_logged = determine_sampling(EntityType::GATE, result.name, result, user)
54
+ return unless should_log
42
55
  event = StatsigEvent.new($gate_exposure_event)
43
56
  event.user = user
44
57
  metadata = {
45
- gate: gate_name,
46
- gateValue: value.to_s,
47
- ruleID: rule_id || Statsig::Const::EMPTY_STR,
58
+ gate: result.name,
59
+ gateValue: result.gate_value.to_s,
60
+ ruleID: result.rule_id || Statsig::Const::EMPTY_STR,
48
61
  }
62
+ if result.config_version != nil
63
+ metadata[:configVersion] = result.config_version.to_s
64
+ end
49
65
  if @debug_info != nil
50
66
  metadata[:debugInfo] = @debug_info
51
67
  end
68
+ unless result.override_config_name.nil?
69
+ metadata[:overrideConfigName] = result.override_config_name
70
+ end
52
71
  return false if not is_unique_exposure(user, $gate_exposure_event, metadata)
53
72
  event.metadata = metadata
73
+ event.statsig_metadata = {}
54
74
 
55
- event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
75
+ event.secondary_exposures = result.secondary_exposures.is_a?(Array) ? result.secondary_exposures : []
56
76
 
57
- safe_add_eval_details(eval_details, event)
77
+ safe_add_eval_details(result.evaluation_details, event)
58
78
  safe_add_exposure_context(context, event)
79
+ safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
59
80
  log_event(event)
60
81
  end
61
82
 
62
- def log_config_exposure(user, config_name, rule_id, secondary_exposures, eval_details, context = nil)
83
+ def log_config_exposure(user, result, context = nil)
84
+ should_log, logged_sampling_rate, shadow_logged = determine_sampling(EntityType::CONFIG, result.name, result, user)
85
+ return unless should_log
63
86
  event = StatsigEvent.new($config_exposure_event)
64
87
  event.user = user
65
88
  metadata = {
66
- config: config_name,
67
- ruleID: rule_id || Statsig::Const::EMPTY_STR,
89
+ config: result.name,
90
+ ruleID: result.rule_id || Statsig::Const::EMPTY_STR,
91
+ rulePassed: result.gate_value.to_s,
68
92
  }
93
+ if result.config_version != nil
94
+ metadata[:configVersion] = result.config_version.to_s
95
+ end
69
96
  if @debug_info != nil
70
97
  metadata[:debugInfo] = @debug_info
71
98
  end
99
+ unless result.override_config_name.nil?
100
+ metadata[:overrideConfigName] = result.override_config_name
101
+ end
72
102
  return false if not is_unique_exposure(user, $config_exposure_event, metadata)
73
103
  event.metadata = metadata
74
- event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
104
+ event.secondary_exposures = result.secondary_exposures.is_a?(Array) ? result.secondary_exposures : []
105
+ event.statsig_metadata = {}
75
106
 
76
- safe_add_eval_details(eval_details, event)
107
+ safe_add_eval_details(result.evaluation_details, event)
77
108
  safe_add_exposure_context(context, event)
109
+ safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
78
110
  log_event(event)
79
111
  end
80
112
 
@@ -87,6 +119,9 @@ module Statsig
87
119
  exposures = config_evaluation.secondary_exposures
88
120
  end
89
121
 
122
+ should_log, logged_sampling_rate, shadow_logged = determine_sampling(EntityType::LAYER, config_evaluation.name, config_evaluation, user, allocated_experiment, parameter_name)
123
+ return unless should_log
124
+
90
125
  event = StatsigEvent.new($layer_exposure_event)
91
126
  event.user = user
92
127
  metadata = {
@@ -96,15 +131,20 @@ module Statsig
96
131
  parameterName: parameter_name,
97
132
  isExplicitParameter: String(is_explicit)
98
133
  }
134
+ if config_evaluation.config_version != nil
135
+ metadata[:configVersion] = config_evaluation.config_version.to_s
136
+ end
99
137
  if @debug_info != nil
100
138
  metadata[:debugInfo] = @debug_info
101
139
  end
102
140
  return false unless is_unique_exposure(user, $layer_exposure_event, metadata)
103
141
  event.metadata = metadata
104
142
  event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
143
+ event.statsig_metadata = {}
105
144
 
106
145
  safe_add_eval_details(config_evaluation.evaluation_details, event)
107
146
  safe_add_exposure_context(context, event)
147
+ safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
108
148
  log_event(event)
109
149
  end
110
150
 
@@ -140,6 +180,7 @@ module Statsig
140
180
 
141
181
  def shutdown
142
182
  @background_flush&.exit
183
+ @sampling_key_set.shutdown
143
184
  @logging_pool.shutdown
144
185
  @logging_pool.wait_for_termination(timeout = 3)
145
186
  flush
@@ -159,8 +200,11 @@ module Statsig
159
200
 
160
201
  events_clone = @events
161
202
  @events = []
162
- flush_events = events_clone.map { |e| e.serialize }
163
- @network.post_logs(flush_events, @error_boundary)
203
+ serialized_events = events_clone.map { |e| e.serialize }
204
+
205
+ serialized_events.each_slice(@options.logging_max_buffer_size) do |batch|
206
+ @network.post_logs(batch, @error_boundary)
207
+ end
164
208
  end
165
209
  end
166
210
 
@@ -197,6 +241,18 @@ module Statsig
197
241
  end
198
242
  end
199
243
 
244
+ def safe_add_sampling_metadata(event, logged_sampling_rate = nil, shadow_logged = nil)
245
+ unless logged_sampling_rate.nil?
246
+ event.statsig_metadata["samplingRate"] = logged_sampling_rate
247
+ end
248
+
249
+ unless shadow_logged.nil?
250
+ event.statsig_metadata["shadowLogged"] = shadow_logged
251
+ end
252
+
253
+ event.statsig_metadata["samplingMode"] = @sdk_configs.get_config_string_value("sampling_mode")
254
+ end
255
+
200
256
  def is_unique_exposure(user, event_name, metadata)
201
257
  return true if user.nil?
202
258
  @deduper.clear if @deduper.size > 10000
@@ -214,5 +270,69 @@ module Statsig
214
270
  @deduper.add(key)
215
271
  true
216
272
  end
273
+
274
+ def determine_sampling(type, name, result, user, exp_name = "", param_name = "")
275
+ begin
276
+ shadow_should_log, logged_sampling_rate = true, nil
277
+ env = @options.environment&.dig(:tier)
278
+ sampling_mode = @sdk_configs.get_config_string_value("sampling_mode")
279
+ special_case_sampling_rate = @sdk_configs.get_config_int_value("special_case_sampling_rate")
280
+ special_case_rules = ["disabled", "default", ""]
281
+
282
+ if sampling_mode.nil? || sampling_mode == "none" || env != "production"
283
+ return true, nil, nil
284
+ end
285
+
286
+ return true, nil, nil if result.forward_all_exposures
287
+ return true, nil, nil if result.rule_id.end_with?(":override", ":id_override")
288
+ return true, nil, nil if result.has_seen_analytical_gates
289
+
290
+ sampling_set_key = "#{name}_#{result.rule_id}"
291
+ unless @sampling_key_set.contains?(sampling_set_key)
292
+ @sampling_key_set.add(sampling_set_key)
293
+ return true, nil, nil
294
+ end
295
+
296
+ should_sample = result.sampling_rate || special_case_rules.include?(result.rule_id)
297
+ unless should_sample
298
+ return true, nil, nil
299
+ end
300
+
301
+ exposure_key = ""
302
+ case type
303
+ when EntityType::GATE
304
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_gate(name, result.rule_id, result.gate_value, user.user_id, user.custom_ids)
305
+ when EntityType::CONFIG
306
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_config(name, result.rule_id, user.user_id, user.custom_ids)
307
+ when EntityType::LAYER
308
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_layer(name, exp_name, param_name, result.rule_id, user.user_id, user.custom_ids)
309
+ end
310
+
311
+ if result.sampling_rate
312
+ shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, result.sampling_rate)
313
+ logged_sampling_rate = result.sampling_rate
314
+ elsif special_case_rules.include?(result.rule_id) && special_case_sampling_rate
315
+ shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, special_case_sampling_rate)
316
+ logged_sampling_rate = special_case_sampling_rate
317
+ end
318
+
319
+ shadow_logged = if logged_sampling_rate.nil?
320
+ nil
321
+ else
322
+ shadow_should_log ? "logged" : "dropped"
323
+ end
324
+ if sampling_mode == "on"
325
+ return shadow_should_log, logged_sampling_rate, shadow_logged
326
+ elsif sampling_mode == "shadow"
327
+ return true, logged_sampling_rate, shadow_logged
328
+ end
329
+
330
+ return true, nil, nil
331
+ rescue => e
332
+ @error_boundary.log_exception(e, "__determine_sampling")
333
+ return true, nil, nil
334
+ end
335
+ end
336
+
217
337
  end
218
338
  end
@@ -88,6 +88,10 @@ class StatsigOptions
88
88
  # Implements Statsig::Interfaces::IUserPersistentStorage.
89
89
  attr_accessor :user_persistent_storage
90
90
 
91
+ # Disable memoization of evaluation results. When true, each evaluation will be performed fresh.
92
+ # default: false
93
+ attr_accessor :disable_evaluation_memoization
94
+
91
95
  def initialize(
92
96
  environment = nil,
93
97
  download_config_specs_url: nil,
@@ -110,7 +114,8 @@ class StatsigOptions
110
114
  network_timeout: nil,
111
115
  post_logs_retry_limit: 3,
112
116
  post_logs_retry_backoff: nil,
113
- user_persistent_storage: nil
117
+ user_persistent_storage: nil,
118
+ disable_evaluation_memoization: false
114
119
  )
115
120
  @environment = environment.is_a?(Hash) ? environment : nil
116
121
 
@@ -140,6 +145,6 @@ class StatsigOptions
140
145
  @post_logs_retry_limit = post_logs_retry_limit
141
146
  @post_logs_retry_backoff = post_logs_retry_backoff
142
147
  @user_persistent_storage = user_persistent_storage
143
-
148
+ @disable_evaluation_memoization = disable_evaluation_memoization
144
149
  end
145
150
  end
data/lib/statsig_user.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'json'
2
- require 'constants'
2
+ require_relative 'constants'
3
3
  ##
4
4
  # The user object to be evaluated against your Statsig configurations (gates/experiments/dynamic configs).
5
5
  class StatsigUser
data/lib/ttl_set.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'concurrent-ruby'
2
+
3
+ RESET_INTERVAL = 60
4
+
5
+ module Statsig
6
+ class TTLSet
7
+ def initialize
8
+ @store = Concurrent::Set.new
9
+ @reset_interval = RESET_INTERVAL
10
+ @background_reset = periodic_reset
11
+ end
12
+
13
+ def add(key)
14
+ @store.add(key)
15
+ end
16
+
17
+ def contains?(key)
18
+ @store.include?(key)
19
+ end
20
+
21
+ def shutdown
22
+ @background_reset&.exit
23
+ end
24
+
25
+ def periodic_reset
26
+ Thread.new do
27
+ loop do
28
+ sleep @reset_interval
29
+ @store.clear
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+
@@ -1,4 +1,4 @@
1
- require 'statsig_options'
1
+ require_relative 'statsig_options'
2
2
 
3
3
  module Statsig
4
4
  class UserPersistentStorageUtils
@@ -65,15 +65,27 @@ module Statsig
65
65
  if user_persisted_values.nil?
66
66
  user_persisted_values = {}
67
67
  end
68
- user_persisted_values[config_name] = evaluation.to_hash
68
+ hash = evaluation.to_hash
69
+ if hash['json_value'].is_a?(Hash)
70
+ hash['json_value'] = self.class.symbolize_keys(hash['json_value'])
71
+ end
72
+ user_persisted_values[config_name] = hash
69
73
  end
70
74
 
71
75
  private
72
76
 
73
77
  def self.parse(values_string)
74
- return JSON.parse(values_string)
75
- rescue JSON::ParserError
76
- return nil
78
+ return nil if values_string.nil?
79
+
80
+ parsed = JSON.parse(values_string)
81
+ return nil if parsed.nil?
82
+
83
+ parsed.each do |config_name, config_value|
84
+ if config_value.is_a?(Hash) && config_value.key?('json_value')
85
+ config_value['json_value'] = symbolize_keys(config_value['json_value'])
86
+ end
87
+ end
88
+ parsed
77
89
  end
78
90
 
79
91
  def self.stringify(values_object)
@@ -85,5 +97,15 @@ module Statsig
85
97
  def self.get_storage_key(user, id_type)
86
98
  "#{user.get_unit_id(id_type)}:#{id_type}"
87
99
  end
100
+
101
+ def self.symbolize_keys(hash)
102
+ return hash unless hash.is_a?(Hash)
103
+
104
+ symbolized = {}
105
+ hash.each do |key, value|
106
+ symbolized[key.to_sym] = value.is_a?(Hash) ? symbolize_keys(value) : value
107
+ end
108
+ symbolized
109
+ end
88
110
  end
89
111
  end
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: 2.1.0
4
+ version: 2.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-26 00:00:00.000000000 Z
11
+ date: 2025-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -226,14 +226,14 @@ dependencies:
226
226
  requirements:
227
227
  - - "~>"
228
228
  - !ruby/object:Gem::Version
229
- version: 2.15.0
229
+ version: 2.18.0
230
230
  type: :runtime
231
231
  prerelease: false
232
232
  version_requirements: !ruby/object:Gem::Requirement
233
233
  requirements:
234
234
  - - "~>"
235
235
  - !ruby/object:Gem::Version
236
- version: 2.15.0
236
+ version: 2.18.0
237
237
  - !ruby/object:Gem::Dependency
238
238
  name: http
239
239
  requirement: !ruby/object:Gem::Requirement
@@ -340,6 +340,7 @@ files:
340
340
  - lib/layer.rb
341
341
  - lib/memo.rb
342
342
  - lib/network.rb
343
+ - lib/sdk_configs.rb
343
344
  - lib/spec_store.rb
344
345
  - lib/statsig.rb
345
346
  - lib/statsig_driver.rb
@@ -348,13 +349,14 @@ files:
348
349
  - lib/statsig_logger.rb
349
350
  - lib/statsig_options.rb
350
351
  - lib/statsig_user.rb
352
+ - lib/ttl_set.rb
351
353
  - lib/ua_parser.rb
352
354
  - lib/user_persistent_storage_utils.rb
353
355
  homepage: https://rubygems.org/gems/statsig
354
356
  licenses:
355
357
  - ISC
356
358
  metadata: {}
357
- post_install_message:
359
+ post_install_message:
358
360
  rdoc_options: []
359
361
  require_paths:
360
362
  - lib
@@ -370,7 +372,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
370
372
  version: '0'
371
373
  requirements: []
372
374
  rubygems_version: 3.2.33
373
- signing_key:
375
+ signing_key:
374
376
  specification_version: 4
375
377
  summary: Statsig server SDK for Ruby
376
378
  test_files: []