statsig 2.3.0 → 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: 4e52cea71e52cabc3730c8e9740d7208f1ba45378354a8ef1bd0639c1cfbf9a6
4
- data.tar.gz: a2e74a859c70adace695bff8040bcf23e2e981cb35bcfeb5fa143369ceccd90f
3
+ metadata.gz: 0d28e21a68fc183ea6273a6785854c6203da158f7c531c9aed09eabef40aaa85
4
+ data.tar.gz: fa40131ef5bb4136caf4abce626606bf241d856bc4122c46e2c1b6314e17b54c
5
5
  SHA512:
6
- metadata.gz: b472daa96f6a6b6cf43874555c4421d6374c387d3f14683b0d8e7959ffc5b4d2a04d9f72144854ae54e4bcdfd87187e8542ed7b0abd2ac97605fb7e585f7ac0b
7
- data.tar.gz: e1a9505ea5fc5fb27ea9f3a82056932538dc146bdeeaff08d3179bbfffac5cfa3abaf7838c986350b15d0bd1ed9f2392238beb1f5d139658b770659497dd3eb5
6
+ metadata.gz: 74a843bea3b005c2bea0356e38228a0ac93e70c2d0cc67e89ceaca04df5952b0f601a30edec517963c09f10b1dca02889273b8cc541d18391d05926cc9bc4bb7
7
+ data.tar.gz: f22a12cfb6a10a3c85375ce5998d83dd97d129f99a06e599b40322f41d198a9e57ec55a7b228ad558b2acdddbd425f7c9d0074fc4052316941e268378d26d966
data/lib/config_result.rb CHANGED
@@ -20,6 +20,7 @@ module Statsig
20
20
  attr_accessor :include_local_overrides
21
21
  attr_accessor :forward_all_exposures
22
22
  attr_accessor :sampling_rate
23
+ attr_accessor :has_seen_analytical_gates
23
24
 
24
25
  def initialize(
25
26
  name:,
@@ -39,7 +40,8 @@ module Statsig
39
40
  config_version: nil,
40
41
  include_local_overrides: true,
41
42
  forward_all_exposures: false,
42
- sampling_rate: nil
43
+ sampling_rate: nil,
44
+ has_seen_analytical_gates: false
43
45
  )
44
46
  @name = name
45
47
  @gate_value = gate_value
@@ -60,6 +62,7 @@ module Statsig
60
62
  @include_local_overrides = include_local_overrides
61
63
  @forward_all_exposures = forward_all_exposures
62
64
  @sampling_rate = sampling_rate
65
+ @has_seen_analytical_gates = has_seen_analytical_gates
63
66
  end
64
67
 
65
68
  def self.from_user_persisted_values(config_name, user_persisted_values)
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
@@ -302,6 +449,7 @@ module Statsig
302
449
 
303
450
  def eval_spec(config_name, user, config, end_result, is_nested: false)
304
451
  config[:rules].each do |rule|
452
+ end_result.sampling_rate = rule[:samplingRate]
305
453
  eval_rule(user, rule, end_result)
306
454
 
307
455
  if end_result.gate_value
@@ -357,6 +505,21 @@ module Statsig
357
505
  end
358
506
  end
359
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
+
360
523
  def finalize_secondary_exposures(end_result)
361
524
  end_result.secondary_exposures = clean_exposures(end_result.secondary_exposures)
362
525
  end_result.undelegated_sec_exps = clean_exposures(end_result.undelegated_sec_exps)
@@ -427,6 +590,9 @@ module Statsig
427
590
  return true
428
591
  when Const::CND_PASS_GATE, Const::CND_FAIL_GATE
429
592
  result = eval_nested_gate(target, user, end_result)
593
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
594
+ end_result.has_seen_analytical_gates = true
595
+ end
430
596
  return type == Const::CND_PASS_GATE ? result : !result
431
597
  when Const::CND_MULTI_PASS_GATE, Const::CND_MULTI_FAIL_GATE
432
598
  return eval_nested_gates(target, type, user, end_result)
@@ -614,7 +780,9 @@ module Statsig
614
780
  is_multi_pass_gate_type = condition_type == Const::CND_MULTI_PASS_GATE
615
781
  gate_names.each { |gate_name|
616
782
  result = eval_nested_gate(gate_name, user, end_result)
617
-
783
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
784
+ end_result.has_seen_analytical_gates = true
785
+ end
618
786
  if is_multi_pass_gate_type == result
619
787
  has_passing_gate = true
620
788
  break
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.0',
373
+ 'sdkVersion' => '2.4.0',
374
374
  'languageVersion' => RUBY_VERSION
375
375
  }
376
376
  end
@@ -276,6 +276,7 @@ module Statsig
276
276
 
277
277
  return true, nil, nil if result.forward_all_exposures
278
278
  return true, nil, nil if result.rule_id.end_with?(":override", ":id_override")
279
+ return true, nil, nil if result.has_seen_analytical_gates
279
280
 
280
281
  sampling_set_key = "#{name}_#{result.rule_id}"
281
282
  unless @sampling_key_set.contains?(sampling_set_key)
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.0
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-02-25 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