statsig 2.2.2 → 2.3.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: 24c2882bd3f5fbf4aeaaac8889ec9eff344202c962b655ce5f638b0beb0d4118
4
- data.tar.gz: fe2d3822d6ff0fea02710b5c2f07b7ba6ca900cc2b1abc52269b8b0a2e26f585
3
+ metadata.gz: 4e52cea71e52cabc3730c8e9740d7208f1ba45378354a8ef1bd0639c1cfbf9a6
4
+ data.tar.gz: a2e74a859c70adace695bff8040bcf23e2e981cb35bcfeb5fa143369ceccd90f
5
5
  SHA512:
6
- metadata.gz: 723cd162782fcad1dda2858102ea92a21c1b58989e6f8d7a399d64f5f0944bdc044fbfe1aa53110745b4a821779ea8b371963834db9e72e20af4c90f71ede860
7
- data.tar.gz: 45c320a711d630d1b25e36094ba1f2007e287457609635dcd6d27ea3ab85e802a85517d62d926cd1108a0f453ba7dd39e4941ff6fea7ead4cdfac605365170ba
6
+ metadata.gz: b472daa96f6a6b6cf43874555c4421d6374c387d3f14683b0d8e7959ffc5b4d2a04d9f72144854ae54e4bcdfd87187e8542ed7b0abd2ac97605fb7e585f7ac0b
7
+ data.tar.gz: e1a9505ea5fc5fb27ea9f3a82056932538dc146bdeeaff08d3179bbfffac5cfa3abaf7838c986350b15d0bd1ed9f2392238beb1f5d139658b770659497dd3eb5
data/lib/config_result.rb CHANGED
@@ -18,6 +18,8 @@ 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
21
23
 
22
24
  def initialize(
23
25
  name:,
@@ -35,7 +37,9 @@ module Statsig
35
37
  disable_evaluation_details: false,
36
38
  disable_exposures: false,
37
39
  config_version: nil,
38
- include_local_overrides: true
40
+ include_local_overrides: true,
41
+ forward_all_exposures: false,
42
+ sampling_rate: nil
39
43
  )
40
44
  @name = name
41
45
  @gate_value = gate_value
@@ -54,6 +58,8 @@ module Statsig
54
58
  @disable_exposures = disable_exposures
55
59
  @config_version = config_version
56
60
  @include_local_overrides = include_local_overrides
61
+ @forward_all_exposures = forward_all_exposures
62
+ @sampling_rate = sampling_rate
57
63
  end
58
64
 
59
65
  def self.from_user_persisted_values(config_name, user_persisted_values)
data/lib/evaluator.rb CHANGED
@@ -325,6 +325,7 @@ module Statsig
325
325
  end_result.id_type = config[:idType]
326
326
  end_result.target_app_ids = config[:targetAppIDs]
327
327
  end_result.gate_value = did_pass
328
+ end_result.forward_all_exposures = config[:forwardAllExposures]
328
329
  if config[:entity] == Const::TYPE_FEATURE_GATE
329
330
  end_result.gate_value = did_pass ? rule[:returnValue] == true : config[:defaultValue] == true
330
331
  end
@@ -340,6 +341,7 @@ module Statsig
340
341
  end_result.group_name = rule[:groupName]
341
342
  end_result.is_experiment_group = rule[:isExperimentGroup] == true
342
343
  end_result.rule_id = rule[:id]
344
+ end_result.sampling_rate = rule[:samplingRate]
343
345
  end
344
346
 
345
347
  unless end_result.disable_evaluation_details
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.0',
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,68 @@ 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
+
280
+ sampling_set_key = "#{name}_#{result.rule_id}"
281
+ unless @sampling_key_set.contains?(sampling_set_key)
282
+ @sampling_key_set.add(sampling_set_key)
283
+ return true, nil, nil
284
+ end
285
+
286
+ should_sample = result.sampling_rate || special_case_rules.include?(result.rule_id)
287
+ unless should_sample
288
+ return true, nil, nil
289
+ end
290
+
291
+ exposure_key = ""
292
+ case type
293
+ when EntityType::GATE
294
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_gate(name, result.rule_id, result.gate_value, user.user_id, user.custom_ids)
295
+ when EntityType::CONFIG
296
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_config(name, result.rule_id, user.user_id, user.custom_ids)
297
+ when EntityType::LAYER
298
+ exposure_key = Statsig::HashUtils.compute_dedupe_key_for_layer(name, exp_name, param_name, result.rule_id, user.user_id, user.custom_ids)
299
+ end
300
+
301
+ if result.sampling_rate
302
+ shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, result.sampling_rate)
303
+ logged_sampling_rate = result.sampling_rate
304
+ elsif special_case_rules.include?(result.rule_id) && special_case_sampling_rate
305
+ shadow_should_log = Statsig::HashUtils.is_hash_in_sampling_rate(exposure_key, special_case_sampling_rate)
306
+ logged_sampling_rate = special_case_sampling_rate
307
+ end
308
+
309
+ shadow_logged = if logged_sampling_rate.nil?
310
+ nil
311
+ else
312
+ shadow_should_log ? "logged" : "dropped"
313
+ end
314
+ if sampling_mode == "on"
315
+ return shadow_should_log, logged_sampling_rate, shadow_logged
316
+ elsif sampling_mode == "shadow"
317
+ return true, logged_sampling_rate, shadow_logged
318
+ end
319
+
320
+ return true, nil, nil
321
+ rescue => e
322
+ @error_boundary.log_exception(e, "__determine_sampling")
323
+ return true, nil, nil
324
+ end
325
+ end
326
+
227
327
  end
228
328
  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.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-03 00:00:00.000000000 Z
11
+ date: 2025-02-25 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