statsig 1.29.0.pre.beta.1 → 1.30.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: e279228e9eb701b7bcf06ac68ada05a4403f1d046c4ddc3343c87cf86d6be049
4
- data.tar.gz: 3b110f44f460c9924aa909c1e9723094189a0fe4a20c6f7572c997cf40968e29
3
+ metadata.gz: 72119a11268473774b98457f89017155813cdaab4c19b8f07e1aad370ae6dd17
4
+ data.tar.gz: 69cb96c04c6b9c322bb239332b8c406a6b543bb42b0cab6da9e641d44fa15371
5
5
  SHA512:
6
- metadata.gz: a4871a883213685bc1d6c537367f4de78cacc81b0cec6929853b57c80439895e0ed49d797ef5bb7733c5dce73df9c0bb1aa5ef4b8c3563351468b557f3e473b4
7
- data.tar.gz: fbdc2952ed68a92640268c6931b59521e641d22448c2c8a897521637951d7c20ea6f0f6d4354252c184a8a1c0b9e03861f9b047c9634165998f6a9218660e7e6
6
+ metadata.gz: d75c0a0d9c6529843c554d05b3ae49e6d863cd01da11f932e679bddddffab4287ef036af37c3cd8fff9310c10cd6758bc6e5435f78626245c5d1f315b91e09eb
7
+ data.tar.gz: b9886d0b78d1491393cfacdea4c1f4cdeae1d2a993a0f5a0875011be1083bb63e4a7721fb3a007869eaa7eaf69ad300438d8038c099e88277958d4daf6d226f7
@@ -42,7 +42,7 @@ module ClientInitializeHelpers
42
42
  target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
43
43
  config_target_apps = config_spec['targetAppIDs']
44
44
 
45
- unless target_app_id.nil? || config_target_apps.nil? || config_target_apps.include?(target_app_id)
45
+ unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
46
46
  return nil
47
47
  end
48
48
 
data/lib/config_result.rb CHANGED
@@ -18,6 +18,7 @@ module Statsig
18
18
  attr_accessor :evaluation_details
19
19
  attr_accessor :group_name
20
20
  attr_accessor :id_type
21
+ attr_accessor :target_app_ids
21
22
 
22
23
  def initialize(
23
24
  name,
@@ -25,12 +26,13 @@ module Statsig
25
26
  json_value = {},
26
27
  rule_id = '',
27
28
  secondary_exposures = [],
28
- config_delegate = '',
29
+ config_delegate = nil,
29
30
  explicit_parameters = [],
30
31
  is_experiment_group: false,
31
32
  evaluation_details: nil,
32
33
  group_name: nil,
33
- id_type: '')
34
+ id_type: '',
35
+ target_app_ids: nil)
34
36
  @name = name
35
37
  @gate_value = gate_value
36
38
  @json_value = json_value
@@ -43,6 +45,7 @@ module Statsig
43
45
  @evaluation_details = evaluation_details
44
46
  @group_name = group_name
45
47
  @id_type = id_type
48
+ @target_app_ids = target_app_ids
46
49
  end
47
50
 
48
51
  sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
@@ -62,7 +65,9 @@ module Statsig
62
65
  hash['rule_id'],
63
66
  hash['secondary_exposures'],
64
67
  evaluation_details: EvaluationDetails.persisted(hash['config_sync_time'], hash['init_time']),
65
- group_name: hash['group_name']
68
+ group_name: hash['group_name'],
69
+ id_type: hash['id_type'],
70
+ target_app_ids: hash['target_app_ids']
66
71
  )
67
72
  end
68
73
 
@@ -75,7 +80,9 @@ module Statsig
75
80
  secondary_exposures: @secondary_exposures,
76
81
  config_sync_time: @evaluation_details.config_sync_time,
77
82
  init_time: @init_time,
78
- group_name: @group_name
83
+ group_name: @group_name,
84
+ id_type: @id_type,
85
+ target_app_ids: @target_app_ids
79
86
  }
80
87
  end
81
88
  end
@@ -24,6 +24,7 @@ module Statsig
24
24
  end
25
25
 
26
26
  puts '[Statsig]: An unexpected exception occurred.'
27
+ puts e.message
27
28
  log_exception(e, tag: caller)
28
29
  res = recover.call
29
30
  end
@@ -45,6 +46,7 @@ module Statsig
45
46
  'STATSIG-API-KEY' => @sdk_key,
46
47
  'STATSIG-SDK-TYPE' => meta['sdkType'],
47
48
  'STATSIG-SDK-VERSION' => meta['sdkVersion'],
49
+ 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
48
50
  'Content-Type' => 'application/json; charset=UTF-8'
49
51
  }).accept(:json)
50
52
  body = {
data/lib/evaluator.rb CHANGED
@@ -31,20 +31,16 @@ module Statsig
31
31
 
32
32
  sig do
33
33
  params(
34
- network: Network,
34
+ store: SpecStore,
35
35
  options: StatsigOptions,
36
- error_callback: T.any(Method, Proc, NilClass),
37
- diagnostics: Diagnostics,
38
- error_boundary: ErrorBoundary,
39
- logger: StatsigLogger,
40
36
  persistent_storage_utils: UserPersistentStorageUtils,
41
37
  ).void
42
38
  end
43
- def initialize(network, options, error_callback, diagnostics, error_boundary, logger, persistent_storage_utils)
44
- @spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics, error_boundary, logger)
39
+ def initialize(store, options, persistent_storage_utils)
45
40
  UAParser.initialize_async
46
41
  CountryLookup.initialize_async
47
42
 
43
+ @spec_store = store
48
44
  @gate_overrides = {}
49
45
  @config_overrides = {}
50
46
  @options = options
@@ -122,7 +118,7 @@ module Statsig
122
118
  @persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name, evaluation)
123
119
  @persistent_storage_utils.save_to_storage(user, config['idType'], user_persisted_values)
124
120
  end
125
- # Otherwise, remove from persisted storage
121
+ # Otherwise, remove from persisted storage
126
122
  else
127
123
  @persistent_storage_utils.remove_experiment_from_storage(user, config['idType'], config_name)
128
124
  evaluation = eval_spec(user, config)
@@ -143,6 +139,26 @@ module Statsig
143
139
  eval_spec(user, @spec_store.get_layer(layer_name))
144
140
  end
145
141
 
142
+ def list_gates
143
+ @spec_store.gates.map { |name, _| name }
144
+ end
145
+
146
+ def list_configs
147
+ @spec_store.configs.map { |name, config| name if config['entity'] == 'dynamic_config' }.compact
148
+ end
149
+
150
+ def list_experiments
151
+ @spec_store.configs.map { |name, config| name if config['entity'] == 'experiment' }.compact
152
+ end
153
+
154
+ def list_autotunes
155
+ @spec_store.configs.map { |name, config| name if config['entity'] == 'autotune' }.compact
156
+ end
157
+
158
+ def list_layers
159
+ @spec_store.layers.map { |name, _| name }
160
+ end
161
+
146
162
  def get_client_initialize_response(user, hash, client_sdk_key)
147
163
  if @spec_store.is_ready_for_checks == false
148
164
  return nil
@@ -226,7 +242,8 @@ module Statsig
226
242
  ),
227
243
  is_experiment_group: result.is_experiment_group,
228
244
  group_name: result.group_name,
229
- id_type: config['idType']
245
+ id_type: config['idType'],
246
+ target_app_ids: config['targetAppIDs']
230
247
  )
231
248
  end
232
249
 
@@ -248,7 +265,8 @@ module Statsig
248
265
  @spec_store.init_reason
249
266
  ),
250
267
  group_name: nil,
251
- id_type: config['idType']
268
+ id_type: config['idType'],
269
+ target_app_ids: config['targetAppIDs']
252
270
  )
253
271
  end
254
272
 
@@ -0,0 +1,70 @@
1
+ # typed: false
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ class FeatureGate
6
+ extend T::Sig
7
+
8
+ sig { returns(String) }
9
+ attr_accessor :name
10
+
11
+ sig { returns(T::Boolean) }
12
+ attr_accessor :value
13
+
14
+ sig { returns(String) }
15
+ attr_accessor :rule_id
16
+
17
+ sig { returns(T.nilable(String)) }
18
+ attr_accessor :group_name
19
+
20
+ sig { returns(String) }
21
+ attr_accessor :id_type
22
+
23
+ sig { returns(T.nilable(Statsig::EvaluationDetails)) }
24
+ attr_accessor :evaluation_details
25
+
26
+ sig { returns(T.nilable(T::Array[String])) }
27
+ attr_accessor :target_app_ids
28
+
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
+ def initialize(
41
+ name,
42
+ value: false,
43
+ rule_id: '',
44
+ group_name: nil,
45
+ id_type: '',
46
+ evaluation_details: nil,
47
+ target_app_ids: nil
48
+ )
49
+ @name = name
50
+ @value = value
51
+ @rule_id = rule_id
52
+ @group_name = group_name
53
+ @id_type = id_type
54
+ @evaluation_details = evaluation_details
55
+ @target_app_ids = target_app_ids
56
+ end
57
+
58
+ sig { params(res: Statsig::ConfigResult).returns(FeatureGate) }
59
+ def self.from_config_result(res)
60
+ new(
61
+ res.name,
62
+ value: res.gate_value,
63
+ rule_id: res.rule_id,
64
+ group_name: res.group_name,
65
+ id_type: res.id_type,
66
+ evaluation_details: res.evaluation_details,
67
+ target_app_ids: res.target_app_ids
68
+ )
69
+ end
70
+ end
data/lib/layer.rb CHANGED
@@ -17,11 +17,25 @@ class Layer
17
17
  sig { returns(String) }
18
18
  attr_accessor :rule_id
19
19
 
20
- sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String, exposure_log_func: T.any(Method, Proc, NilClass)).void }
21
- def initialize(name, value = {}, rule_id = '', exposure_log_func = nil)
20
+ sig { returns(String) }
21
+ attr_accessor :group_name
22
+
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
+ def initialize(name, value = {}, rule_id = '', group_name = nil, allocated_experiment = nil, exposure_log_func = nil)
22
34
  @name = name
23
35
  @value = value
24
36
  @rule_id = rule_id
37
+ @group_name = group_name
38
+ @allocated_experiment = allocated_experiment
25
39
  @exposure_log_func = exposure_log_func
26
40
  end
27
41
 
@@ -58,4 +72,4 @@ class Layer
58
72
 
59
73
  @value[index]
60
74
  end
61
- end
75
+ end
data/lib/network.rb CHANGED
@@ -42,6 +42,7 @@ module Statsig
42
42
  'Content-Type' => 'application/json; charset=UTF-8',
43
43
  'STATSIG-SDK-TYPE' => meta['sdkType'],
44
44
  'STATSIG-SDK-VERSION' => meta['sdkVersion'],
45
+ 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
45
46
  'Accept-Encoding' => 'gzip'
46
47
  }
47
48
  ).accept(:json)
@@ -53,11 +54,42 @@ module Statsig
53
54
  end
54
55
  end
55
56
 
57
+ sig do
58
+ params(since_time: Integer)
59
+ .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
60
+ end
61
+ def download_config_specs(since_time)
62
+ get("download_config_specs/#{@server_secret}.json?sinceTime=#{since_time}")
63
+ end
64
+
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
+ def get(endpoint, retries = 0, backoff = 1)
77
+ request(HttpMethod::GET, endpoint, nil, retries, backoff)
78
+ end
79
+
56
80
  sig do
57
81
  params(endpoint: String, body: String, retries: Integer, backoff: Integer)
58
82
  .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
59
83
  end
60
- def post_helper(endpoint, body, retries = 0, backoff = 1)
84
+ def post(endpoint, body, retries = 0, backoff = 1)
85
+ request(HttpMethod::POST, endpoint, body, retries, backoff)
86
+ end
87
+
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
+ def request(method, endpoint, body, retries = 0, backoff = 1)
61
93
  if @local_mode
62
94
  return nil, nil
63
95
  end
@@ -73,14 +105,20 @@ module Statsig
73
105
  url = URIHelper.build_url(endpoint)
74
106
  begin
75
107
  res = @connection_pool.with do |conn|
76
- conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s).post(url, body: body)
108
+ request = conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
109
+ case method
110
+ when HttpMethod::GET
111
+ request.get(url)
112
+ when HttpMethod::POST
113
+ request.post(url, body: body)
114
+ end
77
115
  end
78
116
  rescue StandardError => e
79
117
  ## network error retry
80
118
  return nil, e unless retries.positive?
81
119
 
82
120
  sleep backoff_adjusted
83
- return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
121
+ return request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
84
122
  end
85
123
  return res, nil if res.status.success?
86
124
 
@@ -91,12 +129,12 @@ module Statsig
91
129
 
92
130
  ## status code retry
93
131
  sleep backoff_adjusted
94
- post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
132
+ request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
95
133
  end
96
134
 
97
135
  def check_gate(user, gate_name)
98
136
  request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
99
- response, = post_helper('check_gate', request_body)
137
+ response, = post('check_gate', request_body)
100
138
  return JSON.parse(response.body) unless response.nil?
101
139
 
102
140
  false
@@ -106,7 +144,7 @@ module Statsig
106
144
 
107
145
  def get_config(user, dynamic_config_name)
108
146
  request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
109
- response, = post_helper('get_config', request_body)
147
+ response, = post('get_config', request_body)
110
148
  return JSON.parse(response.body) unless response.nil?
111
149
 
112
150
  nil
@@ -116,7 +154,7 @@ module Statsig
116
154
 
117
155
  def post_logs(events)
118
156
  json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
119
- post_helper('log_event', json_body, @post_logs_retry_limit)
157
+ post('log_event', json_body, @post_logs_retry_limit)
120
158
  rescue StandardError
121
159
 
122
160
  end
data/lib/spec_store.rb CHANGED
@@ -13,7 +13,7 @@ module Statsig
13
13
  attr_accessor :initial_config_sync_time
14
14
  attr_accessor :init_reason
15
15
 
16
- def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
16
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key)
17
17
  @init_reason = EvaluationReason::UNINITIALIZED
18
18
  @network = network
19
19
  @options = options
@@ -35,6 +35,7 @@ module Statsig
35
35
  @diagnostics = diagnostics
36
36
  @error_boundary = error_boundary
37
37
  @logger = logger
38
+ @secret_key = secret_key
38
39
 
39
40
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
40
41
  options.idlist_threadpool_size,
@@ -121,6 +122,18 @@ module Statsig
121
122
  @specs[:layers][layer_name]
122
123
  end
123
124
 
125
+ def gates
126
+ @specs[:gates]
127
+ end
128
+
129
+ def configs
130
+ @specs[:configs]
131
+ end
132
+
133
+ def layers
134
+ @specs[:layers]
135
+ end
136
+
124
137
  def get_id_list(list_name)
125
138
  @specs[:id_lists][list_name]
126
139
  end
@@ -238,7 +251,7 @@ module Statsig
238
251
 
239
252
  error = nil
240
253
  begin
241
- response, e = @network.post_helper('download_config_specs', JSON.generate({ 'sinceTime' => @last_config_sync_time }))
254
+ response, e = @network.download_config_specs(@last_config_sync_time)
242
255
  code = response&.status.to_i
243
256
  if e.is_a? NetworkError
244
257
  code = e.http_code
@@ -276,6 +289,12 @@ module Statsig
276
289
  specs_json = JSON.parse(specs_string)
277
290
  return false unless specs_json.is_a? Hash
278
291
 
292
+ hashed_sdk_key_used = specs_json['hashed_sdk_key_used']
293
+ unless hashed_sdk_key_used.nil? or hashed_sdk_key_used == Statsig::HashUtils.djb2(@secret_key)
294
+ err_boundary.log_exception(Statsig::InvalidSDKKeyResponse.new)
295
+ return false
296
+ end
297
+
279
298
  @last_config_sync_time = specs_json['time'] || @last_config_sync_time
280
299
  return false unless specs_json['has_updates'] == true &&
281
300
  !specs_json['feature_gates'].nil? &&
@@ -334,7 +353,7 @@ module Statsig
334
353
 
335
354
  def get_id_lists_from_network
336
355
  tracker = @diagnostics.track('get_id_list_sources', 'network_request')
337
- response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
356
+ response, e = @network.post('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
338
357
  code = response&.status.to_i
339
358
  if e.is_a? NetworkError
340
359
  code = e.http_code
data/lib/statsig.rb CHANGED
@@ -26,6 +26,23 @@ module Statsig
26
26
  @shared_instance = StatsigDriver.new(secret_key, options, error_callback)
27
27
  end
28
28
 
29
+ class GetGateOptions < T::Struct
30
+ prop :disable_log_exposure, T::Boolean, default: false
31
+ prop :skip_evaluation, T::Boolean, default: false
32
+ end
33
+
34
+ sig { params(user: StatsigUser, gate_name: String, options: GetGateOptions).returns(FeatureGate) }
35
+ ##
36
+ # Gets the gate, evaluated against the given user. An exposure event will automatically be logged for the gate.
37
+ #
38
+ # @param user A StatsigUser object used for the evaluation
39
+ # @param gate_name The name of the gate being checked
40
+ # @param options Additional options for evaluating the gate
41
+ def self.get_gate(user, gate_name, options = GetGateOptions.new)
42
+ ensure_initialized
43
+ @shared_instance&.get_gate(user, gate_name, options)
44
+ end
45
+
29
46
  class CheckGateOptions < T::Struct
30
47
  prop :disable_log_exposure, T::Boolean, default: false
31
48
  end
@@ -212,6 +229,51 @@ module Statsig
212
229
  @shared_instance&.manually_sync_idlists
213
230
  end
214
231
 
232
+ sig { returns(T::Array[String]) }
233
+ ##
234
+ # Returns a list of all gate names
235
+ #
236
+ def self.list_gates
237
+ ensure_initialized
238
+ @shared_instance&.list_gates
239
+ end
240
+
241
+ sig { returns(T::Array[String]) }
242
+ ##
243
+ # Returns a list of all config names
244
+ #
245
+ def self.list_configs
246
+ ensure_initialized
247
+ @shared_instance&.list_configs
248
+ end
249
+
250
+ sig { returns(T::Array[String]) }
251
+ ##
252
+ # Returns a list of all experiment names
253
+ #
254
+ def self.list_experiments
255
+ ensure_initialized
256
+ @shared_instance&.list_experiments
257
+ end
258
+
259
+ sig { returns(T::Array[String]) }
260
+ ##
261
+ # Returns a list of all autotune names
262
+ #
263
+ def self.list_autotunes
264
+ ensure_initialized
265
+ @shared_instance&.list_autotunes
266
+ end
267
+
268
+ sig { returns(T::Array[String]) }
269
+ ##
270
+ # Returns a list of all layer names
271
+ #
272
+ def self.list_layers
273
+ ensure_initialized
274
+ @shared_instance&.list_layers
275
+ end
276
+
215
277
  sig { void }
216
278
  ##
217
279
  # Stops all Statsig activity and flushes any pending events.
@@ -265,7 +327,8 @@ module Statsig
265
327
  def self.get_statsig_metadata
266
328
  {
267
329
  'sdkType' => 'ruby-server',
268
- 'sdkVersion' => '1.29.0-beta.1',
330
+ 'sdkVersion' => '1.30.0',
331
+ 'languageVersion' => RUBY_VERSION
269
332
  }
270
333
  end
271
334
 
@@ -10,6 +10,7 @@ require 'statsig_options'
10
10
  require 'statsig_user'
11
11
  require 'spec_store'
12
12
  require 'dynamic_config'
13
+ require 'feature_gate'
13
14
  require 'error_boundary'
14
15
  require 'layer'
15
16
  require 'sorbet-runtime'
@@ -38,34 +39,62 @@ class StatsigDriver
38
39
  @net = Statsig::Network.new(secret_key, @options)
39
40
  @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
40
41
  @persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
41
- @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, @persistent_storage_utils)
42
+ @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key)
43
+ @evaluator = Statsig::Evaluator.new(@store, @options, @persistent_storage_utils)
42
44
  tracker.end(success: true)
43
45
 
44
46
  @logger.log_diagnostics_event(@diagnostics)
45
47
  }, caller: __method__.to_s)
46
48
  end
47
49
 
50
+ sig do
51
+ params(
52
+ user: StatsigUser,
53
+ gate_name: String,
54
+ disable_log_exposure: T::Boolean,
55
+ skip_evaluation: T::Boolean
56
+ ).returns(FeatureGate)
57
+ end
58
+ def get_gate_impl(user, gate_name, disable_log_exposure: false, skip_evaluation: false)
59
+ if skip_evaluation
60
+ gate = @store.get_gate(gate_name)
61
+ return FeatureGate.new(gate_name) if gate.nil?
62
+ return FeatureGate.new(gate['name'], target_app_ids: gate['targetAppIDs'])
63
+ end
64
+ user = verify_inputs(user, gate_name, 'gate_name')
65
+
66
+ res = @evaluator.check_gate(user, gate_name)
67
+ if res.nil?
68
+ res = Statsig::ConfigResult.new(gate_name)
69
+ end
70
+
71
+ if res == $fetch_from_server
72
+ res = check_gate_fallback(user, gate_name)
73
+ # exposure logged by the server
74
+ else
75
+ unless disable_log_exposure
76
+ @logger.log_gate_exposure(
77
+ user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
78
+ )
79
+ end
80
+ end
81
+ FeatureGate.from_config_result(res)
82
+ end
83
+
84
+ sig { params(user: StatsigUser, gate_name: String, options: Statsig::GetGateOptions).returns(FeatureGate) }
85
+ def get_gate(user, gate_name, options = Statsig::GetGateOptions.new)
86
+ @err_boundary.capture(task: lambda {
87
+ run_with_diagnostics(task: lambda {
88
+ get_gate_impl(user, gate_name, disable_log_exposure: options.disable_log_exposure, skip_evaluation: options.skip_evaluation)
89
+ }, caller: __method__.to_s)
90
+ }, recover: -> { false }, caller: __method__.to_s)
91
+ end
92
+
48
93
  sig { params(user: StatsigUser, gate_name: String, options: Statsig::CheckGateOptions).returns(T::Boolean) }
49
94
  def check_gate(user, gate_name, options = Statsig::CheckGateOptions.new)
50
95
  @err_boundary.capture(task: lambda {
51
96
  run_with_diagnostics(task: lambda {
52
- user = verify_inputs(user, gate_name, "gate_name")
53
-
54
- res = @evaluator.check_gate(user, gate_name)
55
- if res.nil?
56
- res = Statsig::ConfigResult.new(gate_name)
57
- end
58
-
59
- if res == $fetch_from_server
60
- res = check_gate_fallback(user, gate_name)
61
- # exposure logged by the server
62
- else
63
- if !options.disable_log_exposure
64
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
65
- end
66
- end
67
-
68
- res.gate_value
97
+ get_gate_impl(user, gate_name, disable_log_exposure: options.disable_log_exposure).value
69
98
  }, caller: __method__.to_s)
70
99
  }, recover: -> { false }, caller: __method__.to_s)
71
100
  end
@@ -130,7 +159,7 @@ class StatsigDriver
130
159
  end
131
160
 
132
161
  if res == $fetch_from_server
133
- if res.config_delegate.empty?
162
+ if res.config_delegate.nil?
134
163
  return Layer.new(layer_name)
135
164
  end
136
165
  res = get_config_fallback(user, res.config_delegate)
@@ -140,7 +169,7 @@ class StatsigDriver
140
169
  exposure_log_func = !options.disable_log_exposure ? lambda { |layer, parameter_name|
141
170
  @logger.log_layer_exposure(user, layer, parameter_name, res)
142
171
  } : nil
143
- Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
172
+ Layer.new(res.name, res.json_value, res.rule_id, res.group_name, res.config_delegate, exposure_log_func)
144
173
  }, caller: __method__.to_s)
145
174
  }, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
146
175
  end
@@ -149,7 +178,7 @@ class StatsigDriver
149
178
  def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
150
179
  @err_boundary.capture(task: lambda {
151
180
  res = @evaluator.get_layer(user, layer_name)
152
- layer = Layer.new(layer_name, res.json_value, res.rule_id)
181
+ layer = Layer.new(layer_name, res.json_value, res.rule_id, res.group_name, res.config_delegate)
153
182
  context = { 'is_manual_exposure' => true }
154
183
  @logger.log_layer_exposure(user, layer, parameter_name, res, context)
155
184
  }, caller: __method__.to_s)
@@ -184,6 +213,41 @@ class StatsigDriver
184
213
  }, caller: __method__.to_s)
185
214
  end
186
215
 
216
+ sig { returns(T::Array[String]) }
217
+ def list_gates
218
+ @err_boundary.capture(task: lambda {
219
+ @evaluator.list_gates
220
+ }, caller: __method__.to_s)
221
+ end
222
+
223
+ sig { returns(T::Array[String]) }
224
+ def list_configs
225
+ @err_boundary.capture(task: lambda {
226
+ @evaluator.list_configs
227
+ }, caller: __method__.to_s)
228
+ end
229
+
230
+ sig { returns(T::Array[String]) }
231
+ def list_experiments
232
+ @err_boundary.capture(task: lambda {
233
+ @evaluator.list_experiments
234
+ }, caller: __method__.to_s)
235
+ end
236
+
237
+ sig { returns(T::Array[String]) }
238
+ def list_autotunes
239
+ @err_boundary.capture(task: lambda {
240
+ @evaluator.list_autotunes
241
+ }, caller: __method__.to_s)
242
+ end
243
+
244
+ sig { returns(T::Array[String]) }
245
+ def list_layers
246
+ @err_boundary.capture(task: lambda {
247
+ @evaluator.list_layers
248
+ }, caller: __method__.to_s)
249
+ end
250
+
187
251
  def shutdown
188
252
  @err_boundary.capture(task: lambda {
189
253
  @shutdown = true
@@ -9,4 +9,10 @@ module Statsig
9
9
  class ValueError < StandardError
10
10
 
11
11
  end
12
+
13
+ class InvalidSDKKeyResponse < StandardError
14
+ def initialize(msg="Incorrect SDK Key used to generate response.")
15
+ super
16
+ end
17
+ end
12
18
  end
@@ -22,7 +22,7 @@ class StatsigOptions
22
22
 
23
23
  # The base url used specifically to call download_config_specs.
24
24
  # Takes precedence over api_url_base
25
- sig { returns(T.any(String, NilClass)) }
25
+ sig { returns(String) }
26
26
  attr_accessor :api_url_download_config_specs
27
27
 
28
28
  sig { returns(T.any(Float, Integer)) }
@@ -115,7 +115,7 @@ class StatsigOptions
115
115
  sig do
116
116
  params(
117
117
  environment: T.any(T::Hash[String, String], NilClass),
118
- api_url_base: String,
118
+ api_url_base: T.nilable(String),
119
119
  api_url_download_config_specs: T.any(String, NilClass),
120
120
  rulesets_sync_interval: T.any(Float, Integer),
121
121
  idlists_sync_interval: T.any(Float, Integer),
@@ -137,10 +137,9 @@ class StatsigOptions
137
137
  user_persistent_storage: T.any(Statsig::Interfaces::IUserPersistentStorage, NilClass)
138
138
  ).void
139
139
  end
140
-
141
140
  def initialize(
142
141
  environment = nil,
143
- api_url_base = 'https://statsigapi.net/v1',
142
+ api_url_base = nil,
144
143
  api_url_download_config_specs: nil,
145
144
  rulesets_sync_interval: 10,
146
145
  idlists_sync_interval: 60,
@@ -162,8 +161,8 @@ class StatsigOptions
162
161
  user_persistent_storage: nil
163
162
  )
164
163
  @environment = environment.is_a?(Hash) ? environment : nil
165
- @api_url_base = api_url_base
166
- @api_url_download_config_specs = api_url_download_config_specs
164
+ @api_url_base = api_url_base || 'https://statsigapi.net/v1'
165
+ @api_url_download_config_specs = api_url_download_config_specs || api_url_base || 'https://api.statsigcdn.com/v1'
167
166
  @rulesets_sync_interval = rulesets_sync_interval
168
167
  @idlists_sync_interval = idlists_sync_interval
169
168
  @disable_rulesets_sync = disable_rulesets_sync
data/lib/uri_helper.rb CHANGED
@@ -17,7 +17,7 @@ class URIHelper
17
17
  sig { params(endpoint: String).returns(String) }
18
18
  def build_url(endpoint)
19
19
  api = @options.api_url_base
20
- if endpoint == 'download_config_specs' && !@options.api_url_download_config_specs.nil?
20
+ if endpoint.include?('download_config_specs')
21
21
  api = T.must(@options.api_url_download_config_specs)
22
22
  end
23
23
  unless api.end_with?('/')
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: 1.29.0.pre.beta.1
4
+ version: 1.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-11 00:00:00.000000000 Z
11
+ date: 2024-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -330,6 +330,7 @@ files:
330
330
  - lib/evaluation_details.rb
331
331
  - lib/evaluation_helpers.rb
332
332
  - lib/evaluator.rb
333
+ - lib/feature_gate.rb
333
334
  - lib/hash_utils.rb
334
335
  - lib/id_list.rb
335
336
  - lib/interfaces/data_store.rb
@@ -351,7 +352,7 @@ homepage: https://rubygems.org/gems/statsig
351
352
  licenses:
352
353
  - ISC
353
354
  metadata: {}
354
- post_install_message:
355
+ post_install_message:
355
356
  rdoc_options: []
356
357
  require_paths:
357
358
  - lib
@@ -362,12 +363,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
362
363
  version: 2.5.0
363
364
  required_rubygems_version: !ruby/object:Gem::Requirement
364
365
  requirements:
365
- - - ">"
366
+ - - ">="
366
367
  - !ruby/object:Gem::Version
367
- version: 1.3.1
368
+ version: '0'
368
369
  requirements: []
369
- rubygems_version: 3.3.7
370
- signing_key:
370
+ rubygems_version: 3.2.33
371
+ signing_key:
371
372
  specification_version: 4
372
373
  summary: Statsig server SDK for Ruby
373
374
  test_files: []