statsig 1.29.0 → 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: '093cc05e0b8f3bb7e2f0f019cf36ad33e5abc4c4349d2fb1faa056c1ff5aa341'
4
- data.tar.gz: d66640f8b0c317ca018ff4ccc77d1d370af89b1c84416508631deb610236d519
3
+ metadata.gz: 72119a11268473774b98457f89017155813cdaab4c19b8f07e1aad370ae6dd17
4
+ data.tar.gz: 69cb96c04c6b9c322bb239332b8c406a6b543bb42b0cab6da9e641d44fa15371
5
5
  SHA512:
6
- metadata.gz: f634575320cd1ababfa25816ee8ac5fe27e34c761895b4ba6b20957363e487c295767d65e123ab7d306aa1ce2f535f55cc58570f8767790948d2647ab321150b
7
- data.tar.gz: d563727b175a23afa50cb68cbaf91eb49435bae46b1a48b72d9bf0ff38febeefa86b886c34cc2632f37e357c44f96a1e2586050b41e9ac156697f4105c4f9f0b
6
+ metadata.gz: d75c0a0d9c6529843c554d05b3ae49e6d863cd01da11f932e679bddddffab4287ef036af37c3cd8fff9310c10cd6758bc6e5435f78626245c5d1f315b91e09eb
7
+ data.tar.gz: b9886d0b78d1491393cfacdea4c1f4cdeae1d2a993a0f5a0875011be1083bb63e4a7721fb3a007869eaa7eaf69ad300438d8038c099e88277958d4daf6d226f7
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,
@@ -30,7 +31,8 @@ module Statsig
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/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)
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
@@ -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? &&
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',
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
@@ -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
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
4
+ version: 1.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-13 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