statsig 2.0.1 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '059ef73434d227fde96790307f75b6922f1dbd840f3e7afca68c4600f217263d'
4
- data.tar.gz: 7eb241bdd104280ee49117094859ba9b9801747c32a2b4e8ed043b96e336a812
3
+ metadata.gz: 3870c3be99d3f7295c21311a6e938599bbd0ebd260d4b2c2cb814b48d22bd7f4
4
+ data.tar.gz: 618400ee2baa54a9e4bc18ee7e6e1a1cb017349a513919dd5f5e6caa380fb154
5
5
  SHA512:
6
- metadata.gz: b0b92e135f883955bd187971886822959656c644a9573f96de043c342f2930787152c0c16b293d8ed33cda25559dcd1298f9578056fc049a2042752a168ef0b1
7
- data.tar.gz: c0103fd2da0b70705540e391aa6afc2bfcd81a37a1dfa01791032aeb43b258ff782caf6eba8c8e4188b15d8676a643d8764a2bc9b6f2e9a022b07455fec221c3
6
+ metadata.gz: 47fdd678ef31a026f84bf81a184b39fc171c4d35cb7fb9229ecc2ec835051645cd9ed7b4f6ec2c0bfb92c2b59fe6cd9e742afaff176b438192f6d7b8d07f7ceb
7
+ data.tar.gz: 6db827709c35c1005a22dbf07e81f3ee8d6f316c89ba760beea2ba4adf2a35ae1d5ad2ea97604411733f61e4d167a6ebcbfa1e0dcef05986aacde37aab76dcf0
@@ -92,12 +92,26 @@ module Statsig
92
92
  result[:rule_id] = eval_result.rule_id
93
93
 
94
94
  if include_exposures
95
- result[:secondary_exposures] = eval_result.secondary_exposures
95
+ result[:secondary_exposures] = hash_exposures(eval_result.secondary_exposures, hash_algo)
96
96
  end
97
97
 
98
98
  [hashed_name, result]
99
99
  end
100
100
 
101
+ def self.hash_exposures(exposures, hash_algo)
102
+ return nil if exposures.nil?
103
+ hashed_exposures = []
104
+ exposures.each do |exp|
105
+ hashed_exposures << {
106
+ gate: hash_name(exp[:gate], hash_algo),
107
+ gateValue: exp[:gateValue],
108
+ ruleID: exp[:ruleID]
109
+ }
110
+ end
111
+
112
+ hashed_exposures
113
+ end
114
+
101
115
  def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
102
116
  result[:is_user_in_experiment] = eval_result.is_experiment_group
103
117
  result[:is_experiment_active] = config_spec[:isActive] == true
@@ -132,7 +146,7 @@ module Statsig
132
146
  end
133
147
 
134
148
  if include_exposures
135
- result[:undelegated_secondary_exposures] = eval_result.undelegated_sec_exps || []
149
+ result[:undelegated_secondary_exposures] = hash_exposures(eval_result.undelegated_sec_exps || [], hash_algo)
136
150
  end
137
151
  end
138
152
 
data/lib/constants.rb CHANGED
@@ -123,5 +123,11 @@ module Statsig
123
123
  # API Operators (Segments)
124
124
  OP_IN_SEGMENT_LIST = 'in_segment_list'.freeze
125
125
  OP_NOT_IN_SEGMENT_LIST = 'not_in_segment_list'.freeze
126
+
127
+ # API Operators (Array)
128
+ OP_ARRAY_CONTAINS_ANY = 'array_contains_any'.freeze
129
+ OP_ARRAY_CONTAINS_NONE = 'array_contains_none'.freeze
130
+ OP_ARRAY_CONTAINS_ALL = 'array_contains_all'.freeze
131
+ OP_NOT_ARRAY_CONTAINS_ALL = 'not_array_contains_all'.freeze
126
132
  end
127
133
  end
@@ -53,7 +53,19 @@ class DynamicConfig
53
53
  index_sym = index.to_sym
54
54
  return default_value unless @value.key?(index_sym)
55
55
 
56
- return default_value if @value[index_sym].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
57
- @value[index_sym]
56
+ value = @value[index_sym]
57
+
58
+ case default_value
59
+ when Integer
60
+ return value.to_i if value.is_a?(Numeric) && default_value.is_a?(Integer)
61
+ when Float
62
+ return value.to_f if value.is_a?(Numeric) && default_value.is_a?(Float)
63
+ when TrueClass, FalseClass
64
+ return value if [true, false].include?(value)
65
+ else
66
+ return value if value.class == default_value.class
67
+ end
68
+
69
+ default_value
58
70
  end
59
71
  end
@@ -58,6 +58,18 @@ module EvaluationHelpers
58
58
  end
59
59
  end
60
60
 
61
+ def self.array_contains_any(value, target)
62
+ return false if value.nil? || target.nil?
63
+ value_set = value.to_set
64
+ return target.any? { |item| value_set.include?(item) || value_set.include?(item.to_i) }
65
+ end
66
+
67
+ def self.array_contains_all(value, target)
68
+ return false if value.nil? || target.nil?
69
+ value_set = value.to_set
70
+ return target.all? { |item| value_set.include?(item) || value_set.include?(item.to_i) }
71
+ end
72
+
61
73
  private
62
74
 
63
75
  def self.is_numeric(v)
@@ -73,4 +85,4 @@ module EvaluationHelpers
73
85
  end
74
86
  return time.to_i
75
87
  end
76
- end
88
+ end
data/lib/evaluator.rb CHANGED
@@ -542,6 +542,24 @@ module Statsig
542
542
  a.year == b.year && a.month == b.month && a.day == b.day
543
543
  })
544
544
 
545
+ # array
546
+ when Const::OP_ARRAY_CONTAINS_ANY
547
+ if value.is_a?(Array) && target.is_a?(Array)
548
+ return EvaluationHelpers.array_contains_any(value, target)
549
+ end
550
+ when Const::OP_ARRAY_CONTAINS_NONE
551
+ if value.is_a?(Array) && target.is_a?(Array)
552
+ return !EvaluationHelpers.array_contains_any(value, target)
553
+ end
554
+ when Const::OP_ARRAY_CONTAINS_ALL
555
+ if value.is_a?(Array) && target.is_a?(Array)
556
+ return EvaluationHelpers.array_contains_all(value, target)
557
+ end
558
+ when Const::OP_NOT_ARRAY_CONTAINS_ALL
559
+ if value.is_a?(Array) && target.is_a?(Array)
560
+ return !EvaluationHelpers.array_contains_all(value, target)
561
+ end
562
+
545
563
  # segments
546
564
  when Const::OP_IN_SEGMENT_LIST, Const::OP_NOT_IN_SEGMENT_LIST
547
565
  begin
@@ -670,10 +688,14 @@ module Statsig
670
688
  end
671
689
 
672
690
  def eval_pass_percent(user, rule, config_salt)
691
+ pass_percentage = rule[:passPercentage]
692
+ return true if pass_percentage == 100.0
693
+ return false if pass_percentage == 0.0
694
+
673
695
  unit_id = user.get_unit_id(rule[:idType]) || Const::EMPTY_STR
674
696
  rule_salt = rule[:salt] || rule[:id] || Const::EMPTY_STR
675
697
  hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
676
- return (hash % 10_000) < (rule[:passPercentage] * 100)
698
+ return (hash % 10_000) < (pass_percentage * 100)
677
699
  end
678
700
 
679
701
  def compute_user_hash(user_hash)
data/lib/layer.rb CHANGED
@@ -52,12 +52,31 @@ class Layer
52
52
  index_sym = index.to_sym
53
53
  return default_value unless @value.key?(index_sym)
54
54
 
55
- return default_value if @value[index_sym].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
55
+ value = @value[index_sym]
56
56
 
57
- if @exposure_log_func.is_a? Proc
58
- @exposure_log_func.call(self, index)
57
+ case default_value
58
+ when Integer
59
+ if @exposure_log_func.is_a? Proc
60
+ @exposure_log_func.call(self, index)
61
+ end
62
+ return value.to_i if value.is_a?(Numeric) && default_value.is_a?(Integer)
63
+ when Float
64
+ if @exposure_log_func.is_a? Proc
65
+ @exposure_log_func.call(self, index)
66
+ end
67
+ return value.to_f if value.is_a?(Numeric) && default_value.is_a?(Float)
68
+ when TrueClass, FalseClass
69
+ if @exposure_log_func.is_a? Proc
70
+ @exposure_log_func.call(self, index)
71
+ end
72
+ return value if [true, false].include?(value)
73
+ else
74
+ if @exposure_log_func.is_a? Proc
75
+ @exposure_log_func.call(self, index)
76
+ end
77
+ return value if value.class == default_value.class
59
78
  end
60
79
 
61
- @value[index_sym]
80
+ default_value
62
81
  end
63
82
  end
data/lib/spec_store.rb CHANGED
@@ -44,6 +44,8 @@ module Statsig
44
44
  @secret_key = secret_key
45
45
  @unsupported_configs = Set.new
46
46
 
47
+ startTime = (Time.now.to_f * 1000).to_i
48
+
47
49
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
48
50
  options.idlist_threadpool_size,
49
51
  name: 'statsig-idlist',
@@ -57,7 +59,7 @@ module Statsig
57
59
  else
58
60
  tracker = @diagnostics.track('initialize','bootstrap', 'process')
59
61
  begin
60
- if process_specs(options.bootstrap_values)
62
+ if process_specs(options.bootstrap_values).nil?
61
63
  @init_reason = EvaluationReason::BOOTSTRAP
62
64
  end
63
65
  rescue StandardError
@@ -68,13 +70,15 @@ module Statsig
68
70
  end
69
71
  end
70
72
 
73
+ failure_details = nil
74
+
71
75
  unless @options.data_store.nil?
72
76
  @options.data_store.init
73
- load_config_specs_from_storage_adapter('initialize')
77
+ failure_details = load_config_specs_from_storage_adapter('initialize')
74
78
  end
75
79
 
76
80
  if @init_reason == EvaluationReason::UNINITIALIZED
77
- download_config_specs('initialize')
81
+ failure_details = download_config_specs('initialize')
78
82
  end
79
83
 
80
84
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
@@ -86,12 +90,18 @@ module Statsig
86
90
 
87
91
  @config_sync_thread = spawn_sync_config_specs_thread
88
92
  @id_lists_sync_thread = spawn_sync_id_lists_thread
93
+ endTime = (Time.now.to_f * 1000).to_i
94
+ @initialization_details = {duration: endTime - startTime, isSDKReady: true, configSpecReady: @init_reason != EvaluationReason::UNINITIALIZED, failureDetails: failure_details}
89
95
  end
90
96
 
91
97
  def is_ready_for_checks
92
98
  @last_config_sync_time != 0
93
99
  end
94
100
 
101
+ def get_initialization_details
102
+ @initialization_details
103
+ end
104
+
95
105
  def shutdown
96
106
  @config_sync_thread&.exit
97
107
  @id_lists_sync_thread&.exit
@@ -202,13 +212,19 @@ module Statsig
202
212
  return if cached_values.nil?
203
213
 
204
214
  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)
215
+ failure_details = process_specs(cached_values, from_adapter: true)
216
+ if failure_details.nil?
217
+ @init_reason = EvaluationReason::DATA_ADAPTER
218
+ tracker.end(success: true)
219
+ else
220
+ tracker.end(success: false)
221
+ return download_config_specs(context)
222
+ end
223
+ return failure_details
208
224
  rescue StandardError
209
225
  # Fallback to network
210
226
  tracker.end(success: false)
211
- download_config_specs(context)
227
+ return download_config_specs(context)
212
228
  end
213
229
 
214
230
  def save_rulesets_to_storage_adapter(rulesets_string)
@@ -253,18 +269,21 @@ module Statsig
253
269
  tracker = @diagnostics.track(context, 'download_config_specs', 'network_request')
254
270
 
255
271
  error = nil
272
+ failure_details = nil
256
273
  begin
257
274
  response, e = @network.download_config_specs(@last_config_sync_time)
258
275
  code = response&.status.to_i
259
276
  if e.is_a? NetworkError
260
277
  code = e.http_code
278
+ failure_details = {statusCode: code, exception: e, reason: "CONFIG_SPECS_NETWORK_ERROR"}
261
279
  end
262
280
  tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
263
281
 
264
282
  if e.nil?
265
283
  unless response.nil?
266
284
  tracker = @diagnostics.track(context, 'download_config_specs', 'process')
267
- if process_specs(response.body.to_s)
285
+ failure_details = process_specs(response.body.to_s)
286
+ if failure_details.nil?
268
287
  @init_reason = EvaluationReason::NETWORK
269
288
  end
270
289
  tracker.end(success: @init_reason == EvaluationReason::NETWORK)
@@ -274,59 +293,63 @@ module Statsig
274
293
  @last_config_sync_time)
275
294
  end
276
295
  end
277
-
278
- nil
279
296
  else
280
297
  error = e
281
298
  end
282
299
  rescue StandardError => e
300
+ failure_details = {exception: e, reason: "INTERNAL_ERROR"}
283
301
  error = e
284
302
  end
285
303
 
286
304
  @error_callback.call(error) unless error.nil? or @error_callback.nil?
305
+ return failure_details
287
306
  end
288
307
 
289
308
  def process_specs(specs_string, from_adapter: false)
290
309
  if specs_string.nil?
291
- return false
310
+ return {reason: "EMPTY_SPEC"}
292
311
  end
293
312
 
294
- specs_json = JSON.parse(specs_string, { symbolize_names: true })
295
- return false unless specs_json.is_a? Hash
313
+ begin
314
+ specs_json = JSON.parse(specs_string, { symbolize_names: true })
315
+ return {reason: "PARSE_RESPONSE_ERROR"} unless specs_json.is_a? Hash
296
316
 
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
317
+ hashed_sdk_key_used = specs_json[:hashed_sdk_key_used]
318
+ unless hashed_sdk_key_used.nil? or hashed_sdk_key_used == Statsig::HashUtils.djb2(@secret_key)
319
+ err_boundary.log_exception(Statsig::InvalidSDKKeyResponse.new)
320
+ return {reason: "PARSE_RESPONSE_ERROR"}
321
+ end
302
322
 
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
323
+ new_specs_sync_time = specs_json[:time]
324
+ if new_specs_sync_time.nil? \
325
+ || new_specs_sync_time < @last_config_sync_time \
326
+ || specs_json[:has_updates] != true \
327
+ || specs_json[:feature_gates].nil? \
328
+ || specs_json[:dynamic_configs].nil? \
329
+ || specs_json[:layer_configs].nil?
330
+ return {reason: "PARSE_RESPONSE_ERROR"}
331
+ end
312
332
 
313
- @last_config_sync_time = new_specs_sync_time
314
- @unsupported_configs.clear
333
+ @last_config_sync_time = new_specs_sync_time
334
+ @unsupported_configs.clear
315
335
 
316
- specs_json[:diagnostics]&.each { |key, value| @diagnostics.sample_rates[key.to_s] = value }
336
+ specs_json[:diagnostics]&.each { |key, value| @diagnostics.sample_rates[key.to_s] = value }
317
337
 
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] || {}
338
+ @gates = specs_json[:feature_gates]
339
+ @configs = specs_json[:dynamic_configs]
340
+ @layers = specs_json[:layer_configs]
341
+ @condition_map = specs_json[:condition_map]
342
+ @experiment_to_layer = specs_json[:experiment_to_layer]
343
+ @sdk_keys_to_app_ids = specs_json[:sdk_keys_to_app_ids] || {}
344
+ @hashed_sdk_keys_to_app_ids = specs_json[:hashed_sdk_keys_to_app_ids] || {}
325
345
 
326
- unless from_adapter
327
- save_rulesets_to_storage_adapter(specs_string)
346
+ unless from_adapter
347
+ save_rulesets_to_storage_adapter(specs_string)
348
+ end
349
+ rescue StandardError => e
350
+ return {reason: "PARSE_RESPONSE_ERROR"}
328
351
  end
329
- true
352
+ nil
330
353
  end
331
354
 
332
355
  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
@@ -363,7 +370,7 @@ module Statsig
363
370
  def self.get_statsig_metadata
364
371
  {
365
372
  'sdkType' => 'ruby-server',
366
- 'sdkVersion' => '2.0.1',
373
+ 'sdkVersion' => '2.2.0',
367
374
  'languageVersion' => RUBY_VERSION
368
375
  }
369
376
  end
@@ -43,6 +43,10 @@ class StatsigDriver
43
43
  end
44
44
  end
45
45
 
46
+ def get_initialization_details
47
+ @store.get_initialization_details
48
+ end
49
+
46
50
  def get_gate_impl(
47
51
  user,
48
52
  gate_name,
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.0.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-06 00:00:00.000000000 Z
11
+ date: 2024-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler