statsig 1.29.0 → 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: '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