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 +4 -4
- data/lib/client_initialize_helpers.rb +16 -2
- data/lib/constants.rb +6 -0
- data/lib/dynamic_config.rb +14 -2
- data/lib/evaluation_helpers.rb +13 -1
- data/lib/evaluator.rb +23 -1
- data/lib/layer.rb +23 -4
- data/lib/spec_store.rb +63 -40
- data/lib/statsig.rb +12 -5
- data/lib/statsig_driver.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3870c3be99d3f7295c21311a6e938599bbd0ebd260d4b2c2cb814b48d22bd7f4
|
4
|
+
data.tar.gz: 618400ee2baa54a9e4bc18ee7e6e1a1cb017349a513919dd5f5e6caa380fb154
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/dynamic_config.rb
CHANGED
@@ -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
|
-
|
57
|
-
|
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
|
data/lib/evaluation_helpers.rb
CHANGED
@@ -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) < (
|
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
|
-
|
55
|
+
value = @value[index_sym]
|
56
56
|
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
207
|
-
|
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
|
-
|
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
|
310
|
+
return {reason: "EMPTY_SPEC"}
|
292
311
|
end
|
293
312
|
|
294
|
-
|
295
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
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
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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
|
-
|
314
|
-
|
333
|
+
@last_config_sync_time = new_specs_sync_time
|
334
|
+
@unsupported_configs.clear
|
315
335
|
|
316
|
-
|
336
|
+
specs_json[:diagnostics]&.each { |key, value| @diagnostics.sample_rates[key.to_s] = value }
|
317
337
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
327
|
-
|
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
|
-
|
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)
|
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)
|
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)
|
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)
|
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
|
373
|
+
'sdkVersion' => '2.2.0',
|
367
374
|
'languageVersion' => RUBY_VERSION
|
368
375
|
}
|
369
376
|
end
|
data/lib/statsig_driver.rb
CHANGED
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
|
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-
|
11
|
+
date: 2024-10-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|