statsig 2.2.2 → 2.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24c2882bd3f5fbf4aeaaac8889ec9eff344202c962b655ce5f638b0beb0d4118
4
- data.tar.gz: fe2d3822d6ff0fea02710b5c2f07b7ba6ca900cc2b1abc52269b8b0a2e26f585
3
+ metadata.gz: 1591515ecdce353516857952fae0e460997315f7c764f680a30df065821d36c4
4
+ data.tar.gz: 4818db100a200ade728aa7b3db42008313b975ed951e37ff9d72f46fe269580d
5
5
  SHA512:
6
- metadata.gz: 723cd162782fcad1dda2858102ea92a21c1b58989e6f8d7a399d64f5f0944bdc044fbfe1aa53110745b4a821779ea8b371963834db9e72e20af4c90f71ede860
7
- data.tar.gz: 45c320a711d630d1b25e36094ba1f2007e287457609635dcd6d27ea3ab85e802a85517d62d926cd1108a0f453ba7dd39e4941ff6fea7ead4cdfac605365170ba
6
+ metadata.gz: fe639606633dce22ad66d8216149c2f3db9136c1c2a33799320671df9dc839e06554a91688bc0baf0a638bd29a0258822cc169cc5e01ce8630c5a108d8e883f9
7
+ data.tar.gz: 31912d0a1c2113ce74716987a666695cda53b5532ed154677135332fe17983b145c39d182cc193a510e857083f0290491dfea90df6efe5850935071b3f3dbb64
data/lib/config_result.rb CHANGED
@@ -18,6 +18,9 @@ module Statsig
18
18
  attr_accessor :disable_exposures
19
19
  attr_accessor :config_version
20
20
  attr_accessor :include_local_overrides
21
+ attr_accessor :forward_all_exposures
22
+ attr_accessor :sampling_rate
23
+ attr_accessor :has_seen_analytical_gates
21
24
 
22
25
  def initialize(
23
26
  name:,
@@ -35,7 +38,10 @@ module Statsig
35
38
  disable_evaluation_details: false,
36
39
  disable_exposures: false,
37
40
  config_version: nil,
38
- include_local_overrides: true
41
+ include_local_overrides: true,
42
+ forward_all_exposures: false,
43
+ sampling_rate: nil,
44
+ has_seen_analytical_gates: false
39
45
  )
40
46
  @name = name
41
47
  @gate_value = gate_value
@@ -54,6 +60,9 @@ module Statsig
54
60
  @disable_exposures = disable_exposures
55
61
  @config_version = config_version
56
62
  @include_local_overrides = include_local_overrides
63
+ @forward_all_exposures = forward_all_exposures
64
+ @sampling_rate = sampling_rate
65
+ @has_seen_analytical_gates = has_seen_analytical_gates
57
66
  end
58
67
 
59
68
  def self.from_user_persisted_values(config_name, user_persisted_values)
data/lib/evaluator.rb CHANGED
@@ -302,6 +302,7 @@ module Statsig
302
302
 
303
303
  def eval_spec(config_name, user, config, end_result, is_nested: false)
304
304
  config[:rules].each do |rule|
305
+ end_result.sampling_rate = rule[:samplingRate]
305
306
  eval_rule(user, rule, end_result)
306
307
 
307
308
  if end_result.gate_value
@@ -325,6 +326,7 @@ module Statsig
325
326
  end_result.id_type = config[:idType]
326
327
  end_result.target_app_ids = config[:targetAppIDs]
327
328
  end_result.gate_value = did_pass
329
+ end_result.forward_all_exposures = config[:forwardAllExposures]
328
330
  if config[:entity] == Const::TYPE_FEATURE_GATE
329
331
  end_result.gate_value = did_pass ? rule[:returnValue] == true : config[:defaultValue] == true
330
332
  end
@@ -340,6 +342,7 @@ module Statsig
340
342
  end_result.group_name = rule[:groupName]
341
343
  end_result.is_experiment_group = rule[:isExperimentGroup] == true
342
344
  end_result.rule_id = rule[:id]
345
+ end_result.sampling_rate = rule[:samplingRate]
343
346
  end
344
347
 
345
348
  unless end_result.disable_evaluation_details
@@ -425,6 +428,9 @@ module Statsig
425
428
  return true
426
429
  when Const::CND_PASS_GATE, Const::CND_FAIL_GATE
427
430
  result = eval_nested_gate(target, user, end_result)
431
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
432
+ end_result.has_seen_analytical_gates = true
433
+ end
428
434
  return type == Const::CND_PASS_GATE ? result : !result
429
435
  when Const::CND_MULTI_PASS_GATE, Const::CND_MULTI_FAIL_GATE
430
436
  return eval_nested_gates(target, type, user, end_result)
@@ -612,7 +618,9 @@ module Statsig
612
618
  is_multi_pass_gate_type = condition_type == Const::CND_MULTI_PASS_GATE
613
619
  gate_names.each { |gate_name|
614
620
  result = eval_nested_gate(gate_name, user, end_result)
615
-
621
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
622
+ end_result.has_seen_analytical_gates = true
623
+ end
616
624
  if is_multi_pass_gate_type == result
617
625
  has_passing_gate = true
618
626
  break
data/lib/hash_utils.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  require 'json'
2
+ require 'digest'
3
+
4
+ TWO_TO_THE_63 = 1 << 63
5
+ TWO_TO_THE_64 = 1 << 64
2
6
  module Statsig
3
7
  class HashUtils
4
8
  def self.djb2(input_str)
@@ -32,5 +36,46 @@ module Statsig
32
36
  end
33
37
  return dictionary
34
38
  end
39
+
40
+ def self.bigquery_hash(string)
41
+ digest = Digest::SHA256.digest(string)
42
+ num = digest[0...8].unpack('Q>')[0]
43
+
44
+ if num >= TWO_TO_THE_63
45
+ num - TWO_TO_THE_64
46
+ else
47
+ num
48
+ end
49
+ end
50
+
51
+ def self.is_hash_in_sampling_rate(key, sampling_rate)
52
+ hash_key = bigquery_hash(key)
53
+ hash_key % sampling_rate == 0
54
+ end
55
+
56
+ def self.compute_dedupe_key_for_gate(gate_name, rule_id, value, user_id, custom_ids = nil)
57
+ user_key = compute_user_key(user_id, custom_ids)
58
+ "n:#{gate_name};u:#{user_key}r:#{rule_id};v:#{value}"
59
+ end
60
+
61
+ def self.compute_dedupe_key_for_config(config_name, rule_id, user_id, custom_ids = nil)
62
+ user_key = compute_user_key(user_id, custom_ids)
63
+ "n:#{config_name};u:#{user_key}r:#{rule_id}"
64
+ end
65
+
66
+ def self.compute_dedupe_key_for_layer(layer_name, experiment_name, parameter_name, rule_id, user_id, custom_ids = nil)
67
+ user_key = compute_user_key(user_id, custom_ids)
68
+ "n:#{layer_name};e:#{experiment_name};p:#{parameter_name};u:#{user_key}r:#{rule_id}"
69
+ end
70
+
71
+ def self.compute_user_key(user_id, custom_ids = nil)
72
+ user_key = "u:#{user_id};"
73
+ if custom_ids
74
+ custom_ids.each do |k, v|
75
+ user_key += "#{k}:#{v};"
76
+ end
77
+ end
78
+ user_key
79
+ end
35
80
  end
36
81
  end
@@ -0,0 +1,37 @@
1
+ require 'concurrent-ruby'
2
+
3
+ module Statsig
4
+ class SDKConfigs
5
+ def initialize
6
+ @configs = Concurrent::Hash.new
7
+ @flags = Concurrent::Hash.new
8
+ end
9
+
10
+ def set_flags(new_flags)
11
+ @flags = new_flags || Concurrent::Hash.new
12
+ end
13
+
14
+ def set_configs(new_configs)
15
+ @configs = new_configs || Concurrent::Hash.new
16
+ end
17
+
18
+ def on(flag)
19
+ @flags[flag.to_sym] == true
20
+ end
21
+
22
+ def get_config_num_value(config)
23
+ value = @configs[config.to_sym]
24
+ value.is_a?(Numeric) ? value.to_f : nil
25
+ end
26
+
27
+ def get_config_string_value(config)
28
+ value = @configs[config.to_sym]
29
+ value.is_a?(String) ? value : nil
30
+ end
31
+
32
+ def get_config_int_value(config)
33
+ value = @configs[config.to_sym]
34
+ value.is_a?(Integer) ? value : nil
35
+ end
36
+ end
37
+ end
data/lib/spec_store.rb CHANGED
@@ -20,7 +20,7 @@ module Statsig
20
20
  attr_accessor :hashed_sdk_keys_to_app_ids
21
21
  attr_accessor :unsupported_configs
22
22
 
23
- def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key)
23
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key, sdk_config)
24
24
  @init_reason = EvaluationReason::UNINITIALIZED
25
25
  @network = network
26
26
  @options = options
@@ -43,6 +43,7 @@ module Statsig
43
43
  @logger = logger
44
44
  @secret_key = secret_key
45
45
  @unsupported_configs = Set.new
46
+ @sdk_configs = sdk_config
46
47
 
47
48
  startTime = (Time.now.to_f * 1000).to_i
48
49
 
@@ -342,6 +343,8 @@ module Statsig
342
343
  @experiment_to_layer = specs_json[:experiment_to_layer]
343
344
  @sdk_keys_to_app_ids = specs_json[:sdk_keys_to_app_ids] || {}
344
345
  @hashed_sdk_keys_to_app_ids = specs_json[:hashed_sdk_keys_to_app_ids] || {}
346
+ @sdk_configs.set_flags(specs_json[:sdk_flags])
347
+ @sdk_configs.set_configs(specs_json[:sdk_configs])
345
348
 
346
349
  unless from_adapter
347
350
  save_rulesets_to_storage_adapter(specs_string)
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.2.2',
373
+ 'sdkVersion' => '2.3.1',
374
374
  'languageVersion' => RUBY_VERSION
375
375
  }
376
376
  end
@@ -13,6 +13,7 @@ require 'error_boundary'
13
13
  require 'layer'
14
14
  require 'memo'
15
15
  require 'diagnostics'
16
+ require 'sdk_configs'
16
17
 
17
18
  class StatsigDriver
18
19
 
@@ -27,15 +28,16 @@ class StatsigDriver
27
28
 
28
29
  @err_boundary = Statsig::ErrorBoundary.new(secret_key, !options.nil? && options.local_mode)
29
30
  @err_boundary.capture(caller: __method__) do
30
- @diagnostics = Statsig::Diagnostics.new()
31
+ @diagnostics = Statsig::Diagnostics.new
32
+ @sdk_configs = Statsig::SDKConfigs.new
31
33
  tracker = @diagnostics.track('initialize', 'overall')
32
34
  @options = options || StatsigOptions.new
33
35
  @shutdown = false
34
36
  @secret_key = secret_key
35
37
  @net = Statsig::Network.new(secret_key, @options)
36
- @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
38
+ @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary, @sdk_configs)
37
39
  @persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
38
- @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key)
40
+ @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key, @sdk_configs)
39
41
  @evaluator = Statsig::Evaluator.new(@store, @options, @persistent_storage_utils)
40
42
  tracker.end(success: true)
41
43
 
@@ -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)
@@ -39,6 +50,8 @@ module Statsig
39
50
  end
40
51
 
41
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 = {
@@ -54,15 +67,19 @@ module Statsig
54
67
  end
55
68
  return false if not is_unique_exposure(user, $gate_exposure_event, metadata)
56
69
  event.metadata = metadata
70
+ event.statsig_metadata = {}
57
71
 
58
72
  event.secondary_exposures = result.secondary_exposures.is_a?(Array) ? result.secondary_exposures : []
59
73
 
60
74
  safe_add_eval_details(result.evaluation_details, event)
61
75
  safe_add_exposure_context(context, event)
76
+ safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
62
77
  log_event(event)
63
78
  end
64
79
 
65
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
66
83
  event = StatsigEvent.new($config_exposure_event)
67
84
  event.user = user
68
85
  metadata = {
@@ -79,9 +96,11 @@ module Statsig
79
96
  return false if not is_unique_exposure(user, $config_exposure_event, metadata)
80
97
  event.metadata = metadata
81
98
  event.secondary_exposures = result.secondary_exposures.is_a?(Array) ? result.secondary_exposures : []
99
+ event.statsig_metadata = {}
82
100
 
83
101
  safe_add_eval_details(result.evaluation_details, event)
84
102
  safe_add_exposure_context(context, event)
103
+ safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
85
104
  log_event(event)
86
105
  end
87
106
 
@@ -94,6 +113,9 @@ module Statsig
94
113
  exposures = config_evaluation.secondary_exposures
95
114
  end
96
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
+
97
119
  event = StatsigEvent.new($layer_exposure_event)
98
120
  event.user = user
99
121
  metadata = {
@@ -112,9 +134,11 @@ module Statsig
112
134
  return false unless is_unique_exposure(user, $layer_exposure_event, metadata)
113
135
  event.metadata = metadata
114
136
  event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
137
+ event.statsig_metadata = {}
115
138
 
116
139
  safe_add_eval_details(config_evaluation.evaluation_details, event)
117
140
  safe_add_exposure_context(context, event)
141
+ safe_add_sampling_metadata(event, logged_sampling_rate, shadow_logged)
118
142
  log_event(event)
119
143
  end
120
144
 
@@ -150,6 +174,7 @@ module Statsig
150
174
 
151
175
  def shutdown
152
176
  @background_flush&.exit
177
+ @sampling_key_set.shutdown
153
178
  @logging_pool.shutdown
154
179
  @logging_pool.wait_for_termination(timeout = 3)
155
180
  flush
@@ -207,6 +232,18 @@ module Statsig
207
232
  end
208
233
  end
209
234
 
235
+ def safe_add_sampling_metadata(event, logged_sampling_rate = nil, shadow_logged = nil)
236
+ unless logged_sampling_rate.nil?
237
+ event.statsig_metadata["samplingRate"] = logged_sampling_rate
238
+ end
239
+
240
+ unless shadow_logged.nil?
241
+ event.statsig_metadata["shadowLogged"] = shadow_logged
242
+ end
243
+
244
+ event.statsig_metadata["samplingMode"] = @sdk_configs.get_config_string_value("sampling_mode")
245
+ end
246
+
210
247
  def is_unique_exposure(user, event_name, metadata)
211
248
  return true if user.nil?
212
249
  @deduper.clear if @deduper.size > 10000
@@ -224,5 +261,69 @@ module Statsig
224
261
  @deduper.add(key)
225
262
  true
226
263
  end
264
+
265
+ def determine_sampling(type, name, result, user, exp_name = "", param_name = "")
266
+ begin
267
+ shadow_should_log, logged_sampling_rate = true, nil
268
+ env = @options.environment&.dig(:tier)
269
+ sampling_mode = @sdk_configs.get_config_string_value("sampling_mode")
270
+ special_case_sampling_rate = @sdk_configs.get_config_int_value("special_case_sampling_rate")
271
+ special_case_rules = ["disabled", "default", ""]
272
+
273
+ if sampling_mode.nil? || sampling_mode == "none" || env != "production"
274
+ return true, nil, nil
275
+ end
276
+
277
+ return true, nil, nil if result.forward_all_exposures
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
280
+
281
+ sampling_set_key = "#{name}_#{result.rule_id}"
282
+ unless @sampling_key_set.contains?(sampling_set_key)
283
+ @sampling_key_set.add(sampling_set_key)
284
+ return true, nil, nil
285
+ end
286
+
287
+ should_sample = result.sampling_rate || special_case_rules.include?(result.rule_id)
288
+ unless should_sample
289
+ return true, nil, nil
290
+ end
291
+
292
+ exposure_key = ""
293
+ case type
294
+ when EntityType::GATE
295
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_gate(name, result.rule_id, result.gate_value, user.user_id, user.custom_ids)
296
+ when EntityType::CONFIG
297
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_config(name, result.rule_id, user.user_id, user.custom_ids)
298
+ when EntityType::LAYER
299
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_layer(name, exp_name, param_name, result.rule_id, user.user_id, user.custom_ids)
300
+ end
301
+
302
+ if result.sampling_rate
303
+ shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, result.sampling_rate)
304
+ logged_sampling_rate = result.sampling_rate
305
+ elsif special_case_rules.include?(result.rule_id) && special_case_sampling_rate
306
+ shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, special_case_sampling_rate)
307
+ logged_sampling_rate = special_case_sampling_rate
308
+ end
309
+
310
+ shadow_logged = if logged_sampling_rate.nil?
311
+ nil
312
+ else
313
+ shadow_should_log ? "logged" : "dropped"
314
+ end
315
+ if sampling_mode == "on"
316
+ return shadow_should_log, logged_sampling_rate, shadow_logged
317
+ elsif sampling_mode == "shadow"
318
+ return true, logged_sampling_rate, shadow_logged
319
+ end
320
+
321
+ return true, nil, nil
322
+ rescue => e
323
+ @error_boundary.log_exception(e, "__determine_sampling")
324
+ return true, nil, nil
325
+ end
326
+ end
327
+
227
328
  end
228
329
  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
+
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.2.2
4
+ version: 2.3.1
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-03 00:00:00.000000000 Z
11
+ date: 2025-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -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,6 +349,7 @@ 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