statsig 2.3.1 → 2.4.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/constants.rb +3 -0
- data/lib/evaluator.rb +162 -0
- data/lib/spec_store.rb +16 -0
- data/lib/statsig.rb +1 -1
- 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: 0d28e21a68fc183ea6273a6785854c6203da158f7c531c9aed09eabef40aaa85
|
4
|
+
data.tar.gz: fa40131ef5bb4136caf4abce626606bf241d856bc4122c46e2c1b6314e17b54c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74a843bea3b005c2bea0356e38228a0ac93e70c2d0cc67e89ceaca04df5952b0f601a30edec517963c09f10b1dca02889273b8cc541d18391d05926cc9bc4bb7
|
7
|
+
data.tar.gz: f22a12cfb6a10a3c85375ce5998d83dd97d129f99a06e599b40322f41d198a9e57ec55a7b228ad558b2acdddbd425f7c9d0074fc4052316941e268378d26d966
|
data/lib/constants.rb
CHANGED
@@ -29,6 +29,8 @@ module Statsig
|
|
29
29
|
DISABLED = 'disabled'.freeze
|
30
30
|
DJB2 = 'djb2'.freeze
|
31
31
|
EMAIL = 'email'.freeze
|
32
|
+
EXPLORE = ':explore'.freeze
|
33
|
+
FAILS_TARGETING = 'inlineTargetingRules'.freeze
|
32
34
|
FALSE = 'false'.freeze
|
33
35
|
IP = 'ip'.freeze
|
34
36
|
LAYER = :layer
|
@@ -39,6 +41,7 @@ module Statsig
|
|
39
41
|
OSNAME = 'osname'.freeze
|
40
42
|
OSVERSION = 'osversion'.freeze
|
41
43
|
OVERRIDE = 'override'.freeze
|
44
|
+
PRESTART = 'prestart'.freeze
|
42
45
|
Q_RIGHT_CHEVRON = 'Q>'.freeze
|
43
46
|
STABLEID = 'stableid'.freeze
|
44
47
|
STATSIG_RUBY_SDK = 'statsig-ruby-sdk'.freeze
|
data/lib/evaluator.rb
CHANGED
@@ -122,6 +122,10 @@ module Statsig
|
|
122
122
|
return
|
123
123
|
end
|
124
124
|
|
125
|
+
if eval_cmab(config_name, user, end_result)
|
126
|
+
return
|
127
|
+
end
|
128
|
+
|
125
129
|
unless @spec_store.has_config?(config_name)
|
126
130
|
unsupported_or_unrecognized(config_name, end_result)
|
127
131
|
return
|
@@ -163,6 +167,149 @@ module Statsig
|
|
163
167
|
end
|
164
168
|
end
|
165
169
|
|
170
|
+
def eval_cmab(config_name, user, end_result)
|
171
|
+
return false unless @spec_store.has_cmab_config?(config_name)
|
172
|
+
|
173
|
+
cmab = @spec_store.get_cmab_config(config_name)
|
174
|
+
|
175
|
+
if !cmab[:enabled] || cmab[:groups].length.zero?
|
176
|
+
end_result.rule_id = Const::PRESTART
|
177
|
+
end_result.json_value = cmab[:defaultValue]
|
178
|
+
finalize_cmab_eval_result(cmab, end_result, did_pass: false)
|
179
|
+
return true
|
180
|
+
end
|
181
|
+
|
182
|
+
targeting_gate = cmab[:targetingGateName]
|
183
|
+
unless targeting_gate.nil?
|
184
|
+
check_gate(user, targeting_gate, end_result, is_nested: true)
|
185
|
+
|
186
|
+
gate_value = end_result.gate_value
|
187
|
+
|
188
|
+
unless end_result.disable_exposures
|
189
|
+
new_exposure = {
|
190
|
+
gate: targeting_gate,
|
191
|
+
gateValue: gate_value ? Const::TRUE : Const::FALSE,
|
192
|
+
ruleID: end_result.rule_id
|
193
|
+
}
|
194
|
+
end_result.secondary_exposures.append(new_exposure)
|
195
|
+
end
|
196
|
+
|
197
|
+
if gate_value == false
|
198
|
+
end_result.rule_id = Const::FAILS_TARGETING
|
199
|
+
end_result.json_value = cmab[:defaultValue]
|
200
|
+
finalize_cmab_eval_result(cmab, end_result, did_pass: false)
|
201
|
+
return true
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
cmab_config = cmab[:config]
|
206
|
+
unit_id = user.get_unit_id(cmab[:idType]) || Const::EMPTY_STR
|
207
|
+
salt = cmab[:salt] || config_name
|
208
|
+
hash = compute_user_hash("#{salt}.#{unit_id}")
|
209
|
+
|
210
|
+
# If there is no config assign the user to a random group
|
211
|
+
if cmab_config.nil?
|
212
|
+
group_size = 10_000.0 / cmab[:groups].length
|
213
|
+
group = cmab[:groups][(hash % 10_000) / group_size]
|
214
|
+
end_result.json_value = group[:parameterValues]
|
215
|
+
end_result.rule_id = group[:id]
|
216
|
+
end_result.group_name = group[:name]
|
217
|
+
end_result.is_experiment_group = true
|
218
|
+
finalize_cmab_eval_result(cmab, end_result, did_pass: true)
|
219
|
+
return true
|
220
|
+
end
|
221
|
+
|
222
|
+
should_sample = (hash % 10_000) < cmab[:sampleRate] * 10_000
|
223
|
+
if should_sample && apply_cmab_sampling(cmab, cmab_config, end_result)
|
224
|
+
finalize_cmab_eval_result(cmab, end_result, did_pass: true)
|
225
|
+
return true
|
226
|
+
end
|
227
|
+
apply_cmab_best_group(cmab, cmab_config, user, end_result)
|
228
|
+
finalize_cmab_eval_result(cmab, end_result, did_pass: true)
|
229
|
+
true
|
230
|
+
end
|
231
|
+
|
232
|
+
def apply_cmab_best_group(cmab, cmab_config, user, end_result)
|
233
|
+
higher_better = cmab[:higherIsBetter]
|
234
|
+
best_score = higher_better ? -1_000_000_000 : 1_000_000_000
|
235
|
+
has_score = false
|
236
|
+
best_group = nil
|
237
|
+
cmab[:groups].each do |group|
|
238
|
+
group_id = group[:id]
|
239
|
+
config = cmab_config[group_id.to_sym]
|
240
|
+
next if config.nil?
|
241
|
+
|
242
|
+
weights_numerical = config[:weightsNumerical]
|
243
|
+
weights_categorical = config[:weightsCategorical]
|
244
|
+
|
245
|
+
next if weights_numerical.length.zero? && weights_categorical.length.zero?
|
246
|
+
|
247
|
+
score = 0
|
248
|
+
score += config[:alpha] + config[:intercept]
|
249
|
+
|
250
|
+
weights_categorical.each do |key, weights|
|
251
|
+
value = get_value_from_user(user, key.to_s)
|
252
|
+
next if value.nil?
|
253
|
+
|
254
|
+
if weights.key?(value.to_sym)
|
255
|
+
score += weights[value.to_sym]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
weights_numerical.each do |key, weight|
|
260
|
+
value = get_value_from_user(user, key.to_s)
|
261
|
+
if value.is_a?(Numeric)
|
262
|
+
score += weight * value
|
263
|
+
end
|
264
|
+
end
|
265
|
+
if !has_score || (higher_better && score > best_score) || (!higher_better && score < best_score)
|
266
|
+
best_score = score
|
267
|
+
best_group = group
|
268
|
+
end
|
269
|
+
has_score = true
|
270
|
+
end
|
271
|
+
if best_group.nil?
|
272
|
+
best_group = cmab[:groups][Random.rand(cmab[:groups].length)]
|
273
|
+
end
|
274
|
+
end_result.json_value = best_group[:parameterValues]
|
275
|
+
end_result.rule_id = best_group[:id]
|
276
|
+
end_result.group_name = best_group[:name]
|
277
|
+
end_result.is_experiment_group = true
|
278
|
+
end
|
279
|
+
|
280
|
+
def apply_cmab_sampling(cmab, cmab_config, end_result)
|
281
|
+
total_records = 0.0
|
282
|
+
cmab[:groups].each do |group|
|
283
|
+
group_id = group[:id]
|
284
|
+
config = cmab_config[group_id.to_sym]
|
285
|
+
cur_count = 1.0
|
286
|
+
unless config.nil?
|
287
|
+
cur_count += config[:records]
|
288
|
+
end
|
289
|
+
total_records += 1.0 / cur_count
|
290
|
+
end
|
291
|
+
|
292
|
+
sum = 0.0
|
293
|
+
value = Random.rand
|
294
|
+
cmab[:groups].each do |group|
|
295
|
+
group_id = group[:id]
|
296
|
+
config = cmab_config[group_id.to_sym]
|
297
|
+
cur_count = 1.0
|
298
|
+
unless config.nil?
|
299
|
+
cur_count += config[:records]
|
300
|
+
end
|
301
|
+
sum += 1.0 / (cur_count / total_records)
|
302
|
+
next unless value < sum
|
303
|
+
|
304
|
+
end_result.json_value = group[:parameterValues]
|
305
|
+
end_result.rule_id = group[:id] + Const::EXPLORE
|
306
|
+
end_result.group_name = group[:name]
|
307
|
+
end_result.is_experiment_group = true
|
308
|
+
return true
|
309
|
+
end
|
310
|
+
false
|
311
|
+
end
|
312
|
+
|
166
313
|
def get_layer(user, layer_name, end_result)
|
167
314
|
if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
|
168
315
|
unless end_result.disable_evaluation_details
|
@@ -358,6 +505,21 @@ module Statsig
|
|
358
505
|
end
|
359
506
|
end
|
360
507
|
|
508
|
+
def finalize_cmab_eval_result(config, end_result, did_pass:)
|
509
|
+
end_result.id_type = config[:idType]
|
510
|
+
end_result.target_app_ids = config[:targetAppIDs]
|
511
|
+
end_result.gate_value = did_pass
|
512
|
+
|
513
|
+
unless end_result.disable_evaluation_details
|
514
|
+
end_result.evaluation_details = EvaluationDetails.new(
|
515
|
+
@spec_store.last_config_sync_time,
|
516
|
+
@spec_store.initial_config_sync_time,
|
517
|
+
@spec_store.init_reason
|
518
|
+
)
|
519
|
+
end
|
520
|
+
end_result.config_version = config[:version]
|
521
|
+
end
|
522
|
+
|
361
523
|
def finalize_secondary_exposures(end_result)
|
362
524
|
end_result.secondary_exposures = clean_exposures(end_result.secondary_exposures)
|
363
525
|
end_result.undelegated_sec_exps = clean_exposures(end_result.undelegated_sec_exps)
|
data/lib/spec_store.rb
CHANGED
@@ -19,6 +19,7 @@ module Statsig
|
|
19
19
|
attr_accessor :sdk_keys_to_app_ids
|
20
20
|
attr_accessor :hashed_sdk_keys_to_app_ids
|
21
21
|
attr_accessor :unsupported_configs
|
22
|
+
attr_accessor :cmab_configs
|
22
23
|
|
23
24
|
def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key, sdk_config)
|
24
25
|
@init_reason = EvaluationReason::UNINITIALIZED
|
@@ -33,6 +34,7 @@ module Statsig
|
|
33
34
|
@gates = {}
|
34
35
|
@configs = {}
|
35
36
|
@layers = {}
|
37
|
+
@cmab_configs = {}
|
36
38
|
@condition_map = {}
|
37
39
|
@id_lists = {}
|
38
40
|
@experiment_to_layer = {}
|
@@ -125,6 +127,13 @@ module Statsig
|
|
125
127
|
@layers.key?(layer_name.to_sym)
|
126
128
|
end
|
127
129
|
|
130
|
+
def has_cmab_config?(config_name)
|
131
|
+
if @cmab_configs.nil?
|
132
|
+
return false
|
133
|
+
end
|
134
|
+
@cmab_configs.key?(config_name.to_sym)
|
135
|
+
end
|
136
|
+
|
128
137
|
def get_gate(gate_name)
|
129
138
|
gate_sym = gate_name.to_sym
|
130
139
|
return nil unless has_gate?(gate_sym)
|
@@ -145,6 +154,12 @@ module Statsig
|
|
145
154
|
@layers[layer_sym]
|
146
155
|
end
|
147
156
|
|
157
|
+
def get_cmab_config(config_name)
|
158
|
+
config_sym = config_name.to_sym
|
159
|
+
return nil unless has_cmab_config?(config_sym)
|
160
|
+
@cmab_configs[config_sym]
|
161
|
+
end
|
162
|
+
|
148
163
|
def get_condition(condition_hash)
|
149
164
|
@condition_map[condition_hash.to_sym]
|
150
165
|
end
|
@@ -339,6 +354,7 @@ module Statsig
|
|
339
354
|
@gates = specs_json[:feature_gates]
|
340
355
|
@configs = specs_json[:dynamic_configs]
|
341
356
|
@layers = specs_json[:layer_configs]
|
357
|
+
@cmab_configs = specs_json[:cmab_configs]
|
342
358
|
@condition_map = specs_json[:condition_map]
|
343
359
|
@experiment_to_layer = specs_json[:experiment_to_layer]
|
344
360
|
@sdk_keys_to_app_ids = specs_json[:sdk_keys_to_app_ids] || {}
|
data/lib/statsig.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.
|
4
|
+
version: 2.4.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: 2025-
|
11
|
+
date: 2025-04-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|