statsig 1.29.0.pre.beta.1 → 1.30.0

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