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.
- checksums.yaml +4 -4
- data/lib/api_config.rb +1 -1
- data/lib/client_initialize_helpers.rb +14 -19
- data/lib/config_result.rb +21 -2
- data/lib/constants.rb +3 -0
- data/lib/error_boundary.rb +1 -1
- data/lib/evaluator.rb +346 -18
- data/lib/hash_utils.rb +48 -3
- data/lib/memo.rb +5 -1
- data/lib/network.rb +10 -3
- data/lib/sdk_configs.rb +37 -0
- data/lib/spec_store.rb +97 -49
- data/lib/statsig.rb +59 -8
- data/lib/statsig_driver.rb +76 -28
- data/lib/statsig_event.rb +1 -2
- data/lib/statsig_logger.rb +137 -17
- data/lib/statsig_options.rb +7 -2
- data/lib/statsig_user.rb +1 -1
- data/lib/ttl_set.rb +36 -0
- data/lib/user_persistent_storage_utils.rb +27 -5
- metadata +9 -7
data/lib/statsig_driver.rb
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
data/lib/statsig_logger.rb
CHANGED
|
@@ -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,
|
|
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:
|
|
46
|
-
gateValue:
|
|
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(
|
|
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,
|
|
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:
|
|
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(
|
|
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
|
-
|
|
163
|
-
|
|
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
|
data/lib/statsig_options.rb
CHANGED
|
@@ -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
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
|
-
|
|
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
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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: []
|