statsig 1.31.1 → 1.32.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.
data/lib/feature_gate.rb CHANGED
@@ -1,42 +1,19 @@
1
- # typed: false
2
-
3
- require 'sorbet-runtime'
4
-
5
1
  class FeatureGate
6
- extend T::Sig
7
2
 
8
- sig { returns(String) }
9
3
  attr_accessor :name
10
4
 
11
- sig { returns(T::Boolean) }
12
5
  attr_accessor :value
13
6
 
14
- sig { returns(String) }
15
7
  attr_accessor :rule_id
16
8
 
17
- sig { returns(T.nilable(String)) }
18
9
  attr_accessor :group_name
19
10
 
20
- sig { returns(String) }
21
11
  attr_accessor :id_type
22
12
 
23
- sig { returns(T.nilable(Statsig::EvaluationDetails)) }
24
13
  attr_accessor :evaluation_details
25
14
 
26
- sig { returns(T.nilable(T::Array[String])) }
27
15
  attr_accessor :target_app_ids
28
16
 
29
- sig do
30
- params(
31
- name: String,
32
- value: T::Boolean,
33
- rule_id: String,
34
- group_name: T.nilable(String),
35
- id_type: String,
36
- evaluation_details: T.nilable(Statsig::EvaluationDetails),
37
- target_app_ids: T.nilable(T::Array[String])
38
- ).void
39
- end
40
17
  def initialize(
41
18
  name,
42
19
  value: false,
@@ -55,7 +32,6 @@ class FeatureGate
55
32
  @target_app_ids = target_app_ids
56
33
  end
57
34
 
58
- sig { params(res: Statsig::ConfigResult).returns(FeatureGate) }
59
35
  def self.from_config_result(res)
60
36
  new(
61
37
  res.name,
data/lib/id_list.rb CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
1
+
2
2
  module Statsig
3
3
  class IDList
4
4
  attr_accessor :name
@@ -1,4 +1,4 @@
1
- # typed: true
1
+
2
2
  module Statsig
3
3
  module Interfaces
4
4
  class IDataStore
@@ -1,4 +1,4 @@
1
- # typed: true
1
+
2
2
  module Statsig
3
3
  module Interfaces
4
4
  class IUserPersistentStorage
data/lib/layer.rb CHANGED
@@ -1,6 +1,3 @@
1
- # typed: false
2
-
3
- require 'sorbet-runtime'
4
1
  ##
5
2
  # Contains the current values from Statsig.
6
3
  # Will contain layer default values for all shared parameters in that layer.
@@ -9,37 +6,22 @@ require 'sorbet-runtime'
9
6
  #
10
7
  # Layers Documentation: https://docs.statsig.com/layers
11
8
  class Layer
12
- extend T::Sig
13
9
 
14
- sig { returns(String) }
15
10
  attr_accessor :name
16
11
 
17
- sig { returns(String) }
18
12
  attr_accessor :rule_id
19
13
 
20
- sig { returns(String) }
21
14
  attr_accessor :group_name
22
15
 
23
- sig do
24
- params(
25
- name: String,
26
- value: T::Hash[String, T.untyped],
27
- rule_id: String,
28
- group_name: T.nilable(String),
29
- allocated_experiment: T.nilable(String),
30
- exposure_log_func: T.any(Method, Proc, NilClass)
31
- ).void
32
- end
33
16
  def initialize(name, value = {}, rule_id = '', group_name = nil, allocated_experiment = nil, exposure_log_func = nil)
34
17
  @name = name
35
- @value = value
18
+ @value = value || {}
36
19
  @rule_id = rule_id
37
20
  @group_name = group_name
38
21
  @allocated_experiment = allocated_experiment
39
22
  @exposure_log_func = exposure_log_func
40
23
  end
41
24
 
42
- sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
43
25
  ##
44
26
  # Get the value for the given key (index), falling back to the default_value if it cannot be found.
45
27
  #
@@ -55,7 +37,6 @@ class Layer
55
37
  @value[index]
56
38
  end
57
39
 
58
- sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
59
40
  ##
60
41
  # Get the value for the given key (index), falling back to the default_value if it cannot be found
61
42
  # or is found to have a different type from the default_value.
data/lib/network.rb CHANGED
@@ -1,9 +1,7 @@
1
- # typed: true
2
-
3
1
  require 'http'
4
2
  require 'json'
5
3
  require 'securerandom'
6
- require 'sorbet-runtime'
4
+
7
5
  require 'uri_helper'
8
6
  require 'connection_pool'
9
7
 
@@ -20,9 +18,7 @@ module Statsig
20
18
  end
21
19
 
22
20
  class Network
23
- extend T::Sig
24
21
 
25
- sig { params(server_secret: String, options: StatsigOptions, backoff_mult: Integer).void }
26
22
  def initialize(server_secret, options, backoff_mult = 10)
27
23
  super()
28
24
  URIHelper.initialize(options)
@@ -54,41 +50,18 @@ module Statsig
54
50
  end
55
51
  end
56
52
 
57
- sig do
58
- params(since_time: Integer)
59
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
60
- end
61
53
  def download_config_specs(since_time)
62
54
  get("download_config_specs/#{@server_secret}.json?sinceTime=#{since_time}")
63
55
  end
64
56
 
65
- class HttpMethod < T::Enum
66
- enums do
67
- GET = new
68
- POST = new
69
- end
70
- end
71
-
72
- sig do
73
- params(endpoint: String, retries: Integer, backoff: Integer)
74
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
75
- end
76
57
  def get(endpoint, retries = 0, backoff = 1)
77
- request(HttpMethod::GET, endpoint, nil, retries, backoff)
58
+ request(:GET, endpoint, nil, retries, backoff)
78
59
  end
79
60
 
80
- sig do
81
- params(endpoint: String, body: String, retries: Integer, backoff: Integer)
82
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
83
- end
84
61
  def post(endpoint, body, retries = 0, backoff = 1)
85
- request(HttpMethod::POST, endpoint, body, retries, backoff)
62
+ request(:POST, endpoint, body, retries, backoff)
86
63
  end
87
64
 
88
- sig do
89
- params(method: HttpMethod, endpoint: String, body: T.nilable(String), retries: Integer, backoff: Integer)
90
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
91
- end
92
65
  def request(method, endpoint, body, retries = 0, backoff = 1)
93
66
  if @local_mode
94
67
  return nil, nil
@@ -107,9 +80,9 @@ module Statsig
107
80
  res = @connection_pool.with do |conn|
108
81
  request = conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
109
82
  case method
110
- when HttpMethod::GET
83
+ when :GET
111
84
  request.get(url)
112
- when HttpMethod::POST
85
+ when :POST
113
86
  request.post(url, body: body)
114
87
  end
115
88
  end
@@ -133,7 +106,7 @@ module Statsig
133
106
  end
134
107
 
135
108
  def post_logs(events)
136
- json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
109
+ json_body = JSON.generate({ events: events, statsigMetadata: Statsig.get_statsig_metadata })
137
110
  post('log_event', json_body, @post_logs_retry_limit)
138
111
  rescue StandardError
139
112
 
data/lib/spec_store.rb CHANGED
@@ -1,17 +1,24 @@
1
- # typed: false
2
1
  require 'net/http'
3
2
  require 'uri'
4
3
  require 'evaluation_details'
5
4
  require 'id_list'
6
5
  require 'concurrent-ruby'
7
6
  require 'hash_utils'
7
+ require 'api_config'
8
8
 
9
9
  module Statsig
10
10
  class SpecStore
11
-
12
11
  attr_accessor :last_config_sync_time
13
12
  attr_accessor :initial_config_sync_time
14
13
  attr_accessor :init_reason
14
+ attr_accessor :gates
15
+ attr_accessor :configs
16
+ attr_accessor :layers
17
+ attr_accessor :id_lists
18
+ attr_accessor :experiment_to_layer
19
+ attr_accessor :sdk_keys_to_app_ids
20
+ attr_accessor :hashed_sdk_keys_to_app_ids
21
+ attr_accessor :unsupported_configs
15
22
 
16
23
  def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key)
17
24
  @init_reason = EvaluationReason::UNINITIALIZED
@@ -23,25 +30,24 @@ module Statsig
23
30
  @rulesets_sync_interval = options.rulesets_sync_interval
24
31
  @id_lists_sync_interval = options.idlists_sync_interval
25
32
  @rules_updated_callback = options.rules_updated_callback
26
- @specs = {
27
- :gates => {},
28
- :configs => {},
29
- :layers => {},
30
- :id_lists => {},
31
- :experiment_to_layer => {},
32
- :sdk_keys_to_app_ids => {},
33
- :hashed_sdk_keys_to_app_ids => {}
34
- }
33
+ @gates = {}
34
+ @configs = {}
35
+ @layers = {}
36
+ @id_lists = {}
37
+ @experiment_to_layer = {}
38
+ @sdk_keys_to_app_ids = {}
39
+ @hashed_sdk_keys_to_app_ids = {}
35
40
  @diagnostics = diagnostics
36
41
  @error_boundary = error_boundary
37
42
  @logger = logger
38
43
  @secret_key = secret_key
44
+ @unsupported_configs = Set.new
39
45
 
40
46
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
41
47
  options.idlist_threadpool_size,
42
48
  name: 'statsig-idlist',
43
49
  max_queue: 100,
44
- fallback_policy: :discard,
50
+ fallback_policy: :discard
45
51
  )
46
52
 
47
53
  unless @options.bootstrap_values.nil?
@@ -53,7 +59,7 @@ module Statsig
53
59
  if process_specs(options.bootstrap_values)
54
60
  @init_reason = EvaluationReason::BOOTSTRAP
55
61
  end
56
- rescue
62
+ rescue StandardError
57
63
  puts 'the provided bootstrapValues is not a valid JSON string'
58
64
  ensure
59
65
  tracker.end(success: @init_reason == EvaluationReason::BOOTSTRAP)
@@ -96,70 +102,61 @@ module Statsig
96
102
  end
97
103
 
98
104
  def has_gate?(gate_name)
99
- @specs[:gates].key?(gate_name)
105
+ @gates.key?(gate_name)
100
106
  end
101
107
 
102
108
  def has_config?(config_name)
103
- @specs[:configs].key?(config_name)
109
+ @configs.key?(config_name)
104
110
  end
105
111
 
106
112
  def has_layer?(layer_name)
107
- @specs[:layers].key?(layer_name)
113
+ @layers.key?(layer_name)
108
114
  end
109
115
 
110
116
  def get_gate(gate_name)
111
117
  return nil unless has_gate?(gate_name)
112
- @specs[:gates][gate_name]
118
+
119
+ @gates[gate_name]
113
120
  end
114
121
 
115
122
  def get_config(config_name)
116
123
  return nil unless has_config?(config_name)
117
- @specs[:configs][config_name]
124
+
125
+ @configs[config_name]
118
126
  end
119
127
 
120
128
  def get_layer(layer_name)
121
129
  return nil unless has_layer?(layer_name)
122
- @specs[:layers][layer_name]
123
- end
124
130
 
125
- def gates
126
- @specs[:gates]
127
- end
128
-
129
- def configs
130
- @specs[:configs]
131
- end
132
-
133
- def layers
134
- @specs[:layers]
131
+ @layers[layer_name]
135
132
  end
136
133
 
137
134
  def get_id_list(list_name)
138
- @specs[:id_lists][list_name]
135
+ @id_lists[list_name]
139
136
  end
140
137
 
141
138
  def has_sdk_key?(sdk_key)
142
- @specs[:sdk_keys_to_app_ids].key?(sdk_key)
139
+ @sdk_keys_to_app_ids.key?(sdk_key)
143
140
  end
144
141
 
145
142
  def has_hashed_sdk_key?(hashed_sdk_key)
146
- @specs[:hashed_sdk_keys_to_app_ids].key?(hashed_sdk_key)
143
+ @hashed_sdk_keys_to_app_ids.key?(hashed_sdk_key)
147
144
  end
148
145
 
149
146
  def get_app_id_for_sdk_key(sdk_key)
150
147
  if sdk_key.nil?
151
148
  return nil
152
149
  end
153
- hashed_sdk_key = Statsig::HashUtils.djb2(sdk_key)
150
+
151
+ hashed_sdk_key = Statsig::HashUtils.djb2(sdk_key).to_sym
154
152
  if has_hashed_sdk_key?(hashed_sdk_key)
155
- return @specs[:hashed_sdk_keys_to_app_ids][hashed_sdk_key]
153
+ return @hashed_sdk_keys_to_app_ids[hashed_sdk_key]
156
154
  end
157
- return nil unless has_sdk_key?(sdk_key)
158
- @specs[:sdk_keys_to_app_ids][sdk_key]
159
- end
160
155
 
161
- def get_raw_specs
162
- @specs
156
+ key = sdk_key.to_sym
157
+ return nil unless has_sdk_key?(key)
158
+
159
+ @sdk_keys_to_app_ids[key]
163
160
  end
164
161
 
165
162
  def maybe_restart_background_threads
@@ -211,6 +208,7 @@ module Statsig
211
208
  if @options.data_store.nil?
212
209
  return
213
210
  end
211
+
214
212
  @options.data_store.set(Interfaces::IDataStore::CONFIG_SPECS_KEY, specs_string)
215
213
  end
216
214
 
@@ -264,7 +262,10 @@ module Statsig
264
262
  end
265
263
  tracker.end(success: @init_reason == EvaluationReason::NETWORK)
266
264
 
267
- @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
265
+ unless response.body.nil? or @rules_updated_callback.nil?
266
+ @rules_updated_callback.call(response.body.to_s,
267
+ @last_config_sync_time)
268
+ end
268
269
  end
269
270
 
270
271
  nil
@@ -283,43 +284,41 @@ module Statsig
283
284
  return false
284
285
  end
285
286
 
286
- specs_json = JSON.parse(specs_string)
287
+ specs_json = JSON.parse(specs_string, { symbolize_names: true })
287
288
  return false unless specs_json.is_a? Hash
288
289
 
289
- hashed_sdk_key_used = specs_json['hashed_sdk_key_used']
290
+ hashed_sdk_key_used = specs_json[:hashed_sdk_key_used]
290
291
  unless hashed_sdk_key_used.nil? or hashed_sdk_key_used == Statsig::HashUtils.djb2(@secret_key)
291
292
  err_boundary.log_exception(Statsig::InvalidSDKKeyResponse.new)
292
293
  return false
293
294
  end
294
295
 
295
- @last_config_sync_time = specs_json['time'] || @last_config_sync_time
296
- return false unless specs_json['has_updates'] == true &&
297
- !specs_json['feature_gates'].nil? &&
298
- !specs_json['dynamic_configs'].nil? &&
299
- !specs_json['layer_configs'].nil?
296
+ @last_config_sync_time = specs_json[:time] || @last_config_sync_time
297
+ return false unless specs_json[:has_updates] == true &&
298
+ !specs_json[:feature_gates].nil? &&
299
+ !specs_json[:dynamic_configs].nil? &&
300
+ !specs_json[:layer_configs].nil?
300
301
 
301
- new_gates = {}
302
- new_configs = {}
303
- new_layers = {}
304
- new_exp_to_layer = {}
302
+ @unsupported_configs.clear()
303
+ new_gates = process_configs(specs_json[:feature_gates])
304
+ new_configs = process_configs(specs_json[:dynamic_configs])
305
+ new_layers = process_configs(specs_json[:layer_configs])
305
306
 
306
- specs_json['feature_gates'].each { |gate| new_gates[gate['name']] = gate }
307
- specs_json['dynamic_configs'].each { |config| new_configs[config['name']] = config }
308
- specs_json['layer_configs'].each { |layer| new_layers[layer['name']] = layer }
309
- specs_json['diagnostics']&.each { |key, value| @diagnostics.sample_rates[key] = value }
307
+ new_exp_to_layer = {}
308
+ specs_json[:diagnostics]&.each { |key, value| @diagnostics.sample_rates[key.to_s] = value }
310
309
 
311
- if specs_json['layers'].is_a?(Hash)
312
- specs_json['layers'].each { |layer_name, experiments|
310
+ if specs_json[:layers].is_a?(Hash)
311
+ specs_json[:layers].each do |layer_name, experiments|
313
312
  experiments.each { |experiment_name| new_exp_to_layer[experiment_name] = layer_name }
314
- }
313
+ end
315
314
  end
316
315
 
317
- @specs[:gates] = new_gates
318
- @specs[:configs] = new_configs
319
- @specs[:layers] = new_layers
320
- @specs[:experiment_to_layer] = new_exp_to_layer
321
- @specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
322
- @specs[:hashed_sdk_keys_to_app_ids] = specs_json['hashed_sdk_keys_to_app_ids'] || {}
316
+ @gates = new_gates
317
+ @configs = new_configs
318
+ @layers = new_layers
319
+ @experiment_to_layer = new_exp_to_layer
320
+ @sdk_keys_to_app_ids = specs_json[:sdk_keys_to_app_ids] || {}
321
+ @hashed_sdk_keys_to_app_ids = specs_json[:hashed_sdk_keys_to_app_ids] || {}
323
322
 
324
323
  unless from_adapter
325
324
  save_config_specs_to_storage_adapter(specs_string)
@@ -327,6 +326,17 @@ module Statsig
327
326
  true
328
327
  end
329
328
 
329
+ def process_configs(configs)
330
+ configs.each_with_object({}) do |config, new_configs|
331
+ begin
332
+ new_configs[config[:name]] = APIConfig.from_json(config)
333
+ rescue UnsupportedConfigException => e
334
+ @unsupported_configs.add(config[:name])
335
+ nil
336
+ end
337
+ end
338
+ end
339
+
330
340
  def get_id_lists_from_adapter(context)
331
341
  tracker = @diagnostics.track(context, 'data_store_id_lists', 'fetch')
332
342
  cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
@@ -345,6 +355,7 @@ module Statsig
345
355
  if @options.data_store.nil?
346
356
  return
347
357
  end
358
+
348
359
  @options.data_store.set(Interfaces::IDataStore::ID_LISTS_KEY, id_lists_raw_json)
349
360
  end
350
361
 
@@ -357,7 +368,7 @@ module Statsig
357
368
  end
358
369
  success = e.nil? && !response.nil?
359
370
  tracker.end(statusCode: code, success: success, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
360
- if !success
371
+ unless success
361
372
  return
362
373
  end
363
374
 
@@ -365,16 +376,17 @@ module Statsig
365
376
  server_id_lists = JSON.parse(response)
366
377
  process_id_lists(server_id_lists, context)
367
378
  save_id_lists_to_adapter(response.body.to_s)
368
- rescue
379
+ rescue StandardError
369
380
  # Ignored, will try again
370
381
  end
371
382
  end
372
383
 
373
384
  def process_id_lists(new_id_lists, context, from_adapter: false)
374
- local_id_lists = @specs[:id_lists]
385
+ local_id_lists = @id_lists
375
386
  if !new_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
376
387
  return
377
388
  end
389
+
378
390
  tasks = []
379
391
 
380
392
  tracker = @diagnostics.track(context,
@@ -389,7 +401,7 @@ module Statsig
389
401
  end
390
402
 
391
403
  delete_lists = []
392
- local_id_lists.each do |list_name, list|
404
+ local_id_lists.each do |list_name, _list|
393
405
  unless new_id_lists.key? list_name
394
406
  delete_lists.push list_name
395
407
  end
@@ -425,7 +437,7 @@ module Statsig
425
437
  next
426
438
  end
427
439
 
428
- tasks << Concurrent::Promise.execute(:executor => @id_list_thread_pool) do
440
+ tasks << Concurrent::Promise.execute(executor: @id_list_thread_pool) do
429
441
  if from_adapter
430
442
  get_single_id_list_from_adapter(local_list, context)
431
443
  else
@@ -468,7 +480,7 @@ module Statsig
468
480
  content = res.body.to_s
469
481
  success = process_single_id_list(list, context, content, content_length)
470
482
  save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
471
- rescue
483
+ rescue StandardError
472
484
  tracker.end(success: false)
473
485
  nil
474
486
  end
@@ -479,7 +491,7 @@ module Statsig
479
491
  begin
480
492
  tracker = @diagnostics.track(context, from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
481
493
  unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
482
- @specs[:id_lists].delete(list.name)
494
+ @id_lists.delete(list.name)
483
495
  tracker.end(success: false)
484
496
  return false
485
497
  end
@@ -488,6 +500,7 @@ module Statsig
488
500
  lines.each do |li|
489
501
  line = li.strip
490
502
  next if line.length <= 1
503
+
491
504
  op = line[0]
492
505
  id = line[1..line.length]
493
506
  if op == '+'
@@ -504,11 +517,10 @@ module Statsig
504
517
  end
505
518
  tracker.end(success: true)
506
519
  return true
507
- rescue
520
+ rescue StandardError
508
521
  tracker.end(success: false)
509
522
  return false
510
523
  end
511
524
  end
512
-
513
525
  end
514
526
  end