statsig 1.31.1 → 1.32.0

Sign up to get free protection for your applications and to get access to all the features.
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