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.
- checksums.yaml +4 -4
- data/lib/client_initialize_helpers.rb +13 -17
- data/lib/config_result.rb +16 -1
- data/lib/constants.rb +3 -0
- data/lib/evaluator.rb +258 -8
- data/lib/hash_utils.rb +48 -3
- data/lib/memo.rb +5 -1
- data/lib/sdk_configs.rb +37 -0
- data/lib/spec_store.rb +83 -41
- data/lib/statsig.rb +32 -5
- data/lib/statsig_driver.rb +27 -13
- data/lib/statsig_event.rb +1 -2
- data/lib/statsig_logger.rb +129 -15
- data/lib/statsig_options.rb +7 -2
- data/lib/ttl_set.rb +36 -0
- data/lib/user_persistent_storage_utils.rb +26 -4
- metadata +9 -7
data/lib/statsig_logger.rb
CHANGED
@@ -1,15 +1,24 @@
|
|
1
1
|
require 'constants'
|
2
2
|
require 'statsig_event'
|
3
|
+
require 'ttl_set'
|
3
4
|
require 'concurrent-ruby'
|
5
|
+
require 'hash_utils'
|
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,58 @@ 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
|
52
68
|
return false if not is_unique_exposure(user, $gate_exposure_event, metadata)
|
53
69
|
event.metadata = metadata
|
70
|
+
event.statsig_metadata = {}
|
54
71
|
|
55
|
-
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
72
|
+
event.secondary_exposures = result.secondary_exposures.is_a?(Array) ? result.secondary_exposures : []
|
56
73
|
|
57
|
-
safe_add_eval_details(
|
74
|
+
safe_add_eval_details(result.evaluation_details, event)
|
58
75
|
safe_add_exposure_context(context, event)
|
76
|
+
safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
|
59
77
|
log_event(event)
|
60
78
|
end
|
61
79
|
|
62
|
-
def log_config_exposure(user,
|
80
|
+
def log_config_exposure(user, result, context = nil)
|
81
|
+
should_log, logged_sampling_rate, shadow_logged = determine_sampling(EntityType::CONFIG, result.name, result, user)
|
82
|
+
return unless should_log
|
63
83
|
event = StatsigEvent.new($config_exposure_event)
|
64
84
|
event.user = user
|
65
85
|
metadata = {
|
66
|
-
config:
|
67
|
-
ruleID: rule_id || Statsig::Const::EMPTY_STR,
|
86
|
+
config: result.name,
|
87
|
+
ruleID: result.rule_id || Statsig::Const::EMPTY_STR,
|
88
|
+
rulePassed: result.gate_value.to_s,
|
68
89
|
}
|
90
|
+
if result.config_version != nil
|
91
|
+
metadata[:configVersion] = result.config_version.to_s
|
92
|
+
end
|
69
93
|
if @debug_info != nil
|
70
94
|
metadata[:debugInfo] = @debug_info
|
71
95
|
end
|
72
96
|
return false if not is_unique_exposure(user, $config_exposure_event, metadata)
|
73
97
|
event.metadata = metadata
|
74
|
-
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
98
|
+
event.secondary_exposures = result.secondary_exposures.is_a?(Array) ? result.secondary_exposures : []
|
99
|
+
event.statsig_metadata = {}
|
75
100
|
|
76
|
-
safe_add_eval_details(
|
101
|
+
safe_add_eval_details(result.evaluation_details, event)
|
77
102
|
safe_add_exposure_context(context, event)
|
103
|
+
safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
|
78
104
|
log_event(event)
|
79
105
|
end
|
80
106
|
|
@@ -87,6 +113,9 @@ module Statsig
|
|
87
113
|
exposures = config_evaluation.secondary_exposures
|
88
114
|
end
|
89
115
|
|
116
|
+
should_log, logged_sampling_rate, shadow_logged = determine_sampling(EntityType::LAYER, config_evaluation.name, config_evaluation, user, allocated_experiment, parameter_name)
|
117
|
+
return unless should_log
|
118
|
+
|
90
119
|
event = StatsigEvent.new($layer_exposure_event)
|
91
120
|
event.user = user
|
92
121
|
metadata = {
|
@@ -96,15 +125,20 @@ module Statsig
|
|
96
125
|
parameterName: parameter_name,
|
97
126
|
isExplicitParameter: String(is_explicit)
|
98
127
|
}
|
128
|
+
if config_evaluation.config_version != nil
|
129
|
+
metadata[:configVersion] = config_evaluation.config_version.to_s
|
130
|
+
end
|
99
131
|
if @debug_info != nil
|
100
132
|
metadata[:debugInfo] = @debug_info
|
101
133
|
end
|
102
134
|
return false unless is_unique_exposure(user, $layer_exposure_event, metadata)
|
103
135
|
event.metadata = metadata
|
104
136
|
event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
|
137
|
+
event.statsig_metadata = {}
|
105
138
|
|
106
139
|
safe_add_eval_details(config_evaluation.evaluation_details, event)
|
107
140
|
safe_add_exposure_context(context, event)
|
141
|
+
safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
|
108
142
|
log_event(event)
|
109
143
|
end
|
110
144
|
|
@@ -140,6 +174,7 @@ module Statsig
|
|
140
174
|
|
141
175
|
def shutdown
|
142
176
|
@background_flush&.exit
|
177
|
+
@sampling_key_set.shutdown
|
143
178
|
@logging_pool.shutdown
|
144
179
|
@logging_pool.wait_for_termination(timeout = 3)
|
145
180
|
flush
|
@@ -159,8 +194,11 @@ module Statsig
|
|
159
194
|
|
160
195
|
events_clone = @events
|
161
196
|
@events = []
|
162
|
-
|
163
|
-
|
197
|
+
serialized_events = events_clone.map { |e| e.serialize }
|
198
|
+
|
199
|
+
serialized_events.each_slice(@options.logging_max_buffer_size) do |batch|
|
200
|
+
@network.post_logs(batch, @error_boundary)
|
201
|
+
end
|
164
202
|
end
|
165
203
|
end
|
166
204
|
|
@@ -197,6 +235,18 @@ module Statsig
|
|
197
235
|
end
|
198
236
|
end
|
199
237
|
|
238
|
+
def safe_add_sampling_metadata(event, logged_sampling_rate = nil, shadow_logged = nil)
|
239
|
+
unless logged_sampling_rate.nil?
|
240
|
+
event.statsig_metadata["samplingRate"] = logged_sampling_rate
|
241
|
+
end
|
242
|
+
|
243
|
+
unless shadow_logged.nil?
|
244
|
+
event.statsig_metadata["shadowLogged"] = shadow_logged
|
245
|
+
end
|
246
|
+
|
247
|
+
event.statsig_metadata["samplingMode"] = @sdk_configs.get_config_string_value("sampling_mode")
|
248
|
+
end
|
249
|
+
|
200
250
|
def is_unique_exposure(user, event_name, metadata)
|
201
251
|
return true if user.nil?
|
202
252
|
@deduper.clear if @deduper.size > 10000
|
@@ -214,5 +264,69 @@ module Statsig
|
|
214
264
|
@deduper.add(key)
|
215
265
|
true
|
216
266
|
end
|
267
|
+
|
268
|
+
def determine_sampling(type, name, result, user, exp_name = "", param_name = "")
|
269
|
+
begin
|
270
|
+
shadow_should_log, logged_sampling_rate = true, nil
|
271
|
+
env = @options.environment&.dig(:tier)
|
272
|
+
sampling_mode = @sdk_configs.get_config_string_value("sampling_mode")
|
273
|
+
special_case_sampling_rate = @sdk_configs.get_config_int_value("special_case_sampling_rate")
|
274
|
+
special_case_rules = ["disabled", "default", ""]
|
275
|
+
|
276
|
+
if sampling_mode.nil? || sampling_mode == "none" || env != "production"
|
277
|
+
return true, nil, nil
|
278
|
+
end
|
279
|
+
|
280
|
+
return true, nil, nil if result.forward_all_exposures
|
281
|
+
return true, nil, nil if result.rule_id.end_with?(":override", ":id_override")
|
282
|
+
return true, nil, nil if result.has_seen_analytical_gates
|
283
|
+
|
284
|
+
sampling_set_key = "#{name}_#{result.rule_id}"
|
285
|
+
unless @sampling_key_set.contains?(sampling_set_key)
|
286
|
+
@sampling_key_set.add(sampling_set_key)
|
287
|
+
return true, nil, nil
|
288
|
+
end
|
289
|
+
|
290
|
+
should_sample = result.sampling_rate || special_case_rules.include?(result.rule_id)
|
291
|
+
unless should_sample
|
292
|
+
return true, nil, nil
|
293
|
+
end
|
294
|
+
|
295
|
+
exposure_key = ""
|
296
|
+
case type
|
297
|
+
when EntityType::GATE
|
298
|
+
exposure_key = Statsig::HashUtils.compute_dedupe_key_for_gate(name, result.rule_id, result.gate_value, user.user_id, user.custom_ids)
|
299
|
+
when EntityType::CONFIG
|
300
|
+
exposure_key = Statsig::HashUtils.compute_dedupe_key_for_config(name, result.rule_id, user.user_id, user.custom_ids)
|
301
|
+
when EntityType::LAYER
|
302
|
+
exposure_key = Statsig::HashUtils.compute_dedupe_key_for_layer(name, exp_name, param_name, result.rule_id, user.user_id, user.custom_ids)
|
303
|
+
end
|
304
|
+
|
305
|
+
if result.sampling_rate
|
306
|
+
shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, result.sampling_rate)
|
307
|
+
logged_sampling_rate = result.sampling_rate
|
308
|
+
elsif special_case_rules.include?(result.rule_id) && special_case_sampling_rate
|
309
|
+
shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, special_case_sampling_rate)
|
310
|
+
logged_sampling_rate = special_case_sampling_rate
|
311
|
+
end
|
312
|
+
|
313
|
+
shadow_logged = if logged_sampling_rate.nil?
|
314
|
+
nil
|
315
|
+
else
|
316
|
+
shadow_should_log ? "logged" : "dropped"
|
317
|
+
end
|
318
|
+
if sampling_mode == "on"
|
319
|
+
return shadow_should_log, logged_sampling_rate, shadow_logged
|
320
|
+
elsif sampling_mode == "shadow"
|
321
|
+
return true, logged_sampling_rate, shadow_logged
|
322
|
+
end
|
323
|
+
|
324
|
+
return true, nil, nil
|
325
|
+
rescue => e
|
326
|
+
@error_boundary.log_exception(e, "__determine_sampling")
|
327
|
+
return true, nil, nil
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
217
331
|
end
|
218
332
|
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/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
|
+
|
@@ -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.
|
4
|
+
version: 2.5.5
|
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-08-11 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: []
|