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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1591515ecdce353516857952fae0e460997315f7c764f680a30df065821d36c4
4
- data.tar.gz: 4818db100a200ade728aa7b3db42008313b975ed951e37ff9d72f46fe269580d
3
+ metadata.gz: 0d28e21a68fc183ea6273a6785854c6203da158f7c531c9aed09eabef40aaa85
4
+ data.tar.gz: fa40131ef5bb4136caf4abce626606bf241d856bc4122c46e2c1b6314e17b54c
5
5
  SHA512:
6
- metadata.gz: fe639606633dce22ad66d8216149c2f3db9136c1c2a33799320671df9dc839e06554a91688bc0baf0a638bd29a0258822cc169cc5e01ce8630c5a108d8e883f9
7
- data.tar.gz: 31912d0a1c2113ce74716987a666695cda53b5532ed154677135332fe17983b145c39d182cc193a510e857083f0290491dfea90df6efe5850935071b3f3dbb64
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
@@ -370,7 +370,7 @@ module Statsig
370
370
  def self.get_statsig_metadata
371
371
  {
372
372
  'sdkType' => 'ruby-server',
373
- 'sdkVersion' => '2.3.1',
373
+ 'sdkVersion' => '2.4.0',
374
374
  'languageVersion' => RUBY_VERSION
375
375
  }
376
376
  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.3.1
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-03-06 00:00:00.000000000 Z
11
+ date: 2025-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler