statsig 1.25.2 → 1.33.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,3 @@
1
- # typed: true
2
-
3
1
  require 'config_result'
4
2
  require 'evaluator'
5
3
  require 'network'
@@ -10,15 +8,13 @@ require 'statsig_options'
10
8
  require 'statsig_user'
11
9
  require 'spec_store'
12
10
  require 'dynamic_config'
11
+ require 'feature_gate'
13
12
  require 'error_boundary'
14
13
  require 'layer'
15
- require 'sorbet-runtime'
14
+
16
15
  require 'diagnostics'
17
16
 
18
17
  class StatsigDriver
19
- extend T::Sig
20
-
21
- sig { params(secret_key: String, options: T.any(StatsigOptions, NilClass), error_callback: T.any(Method, Proc, NilClass)).void }
22
18
 
23
19
  def initialize(secret_key, options = nil, error_callback = nil)
24
20
  unless secret_key.start_with?('secret-')
@@ -31,130 +27,163 @@ class StatsigDriver
31
27
 
32
28
  @err_boundary = Statsig::ErrorBoundary.new(secret_key)
33
29
  @err_boundary.capture(task: lambda {
34
- @diagnostics = Statsig::Diagnostics.new('initialize')
35
- tracker = @diagnostics.track('overall')
30
+ @diagnostics = Statsig::Diagnostics.new()
31
+ tracker = @diagnostics.track('initialize', 'overall')
36
32
  @options = options || StatsigOptions.new
37
33
  @shutdown = false
38
34
  @secret_key = secret_key
39
35
  @net = Statsig::Network.new(secret_key, @options)
40
- @logger = Statsig::StatsigLogger.new(@net, @options)
41
- @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics)
42
- tracker.end('success')
36
+ @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
37
+ @persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
38
+ @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key)
39
+ @evaluator = Statsig::Evaluator.new(@store, @options, @persistent_storage_utils)
40
+ tracker.end(success: true)
43
41
 
44
- @logger.log_diagnostics_event(@diagnostics)
45
- })
46
- @err_boundary.logger = @logger
47
- end
48
-
49
- class CheckGateOptions < T::Struct
50
- prop :log_exposure, T::Boolean, default: true
42
+ @logger.log_diagnostics_event(@diagnostics, 'initialize')
43
+ }, caller: __method__.to_s)
51
44
  end
52
45
 
53
- sig { params(user: StatsigUser, gate_name: String, options: CheckGateOptions).returns(T::Boolean) }
54
-
55
- def check_gate(user, gate_name, options = CheckGateOptions.new)
56
- @err_boundary.capture(task: lambda {
57
- user = verify_inputs(user, gate_name, "gate_name")
46
+ def get_gate_impl(
47
+ user,
48
+ gate_name,
49
+ disable_log_exposure: false,
50
+ skip_evaluation: false,
51
+ disable_evaluation_details: false,
52
+ ignore_local_overrides: false
53
+ )
54
+ if skip_evaluation
55
+ gate = @store.get_gate(gate_name)
56
+ return FeatureGate.new(gate_name) if gate.nil?
57
+ return FeatureGate.new(gate.name, target_app_ids: gate.target_app_ids)
58
+ end
59
+ user = verify_inputs(user, gate_name, 'gate_name')
58
60
 
59
- res = @evaluator.check_gate(user, gate_name)
60
- if res.nil?
61
- res = Statsig::ConfigResult.new(gate_name)
62
- end
61
+ res = Statsig::ConfigResult.new(name: gate_name, disable_exposures: disable_log_exposure, disable_evaluation_details: disable_evaluation_details)
62
+ @evaluator.check_gate(user, gate_name, res, ignore_local_overrides: ignore_local_overrides)
63
63
 
64
- if res == $fetch_from_server
65
- res = check_gate_fallback(user, gate_name)
66
- # exposure logged by the server
67
- else
68
- if options.log_exposure
69
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
70
- end
71
- end
64
+ unless disable_log_exposure
65
+ @logger.log_gate_exposure(
66
+ user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
67
+ )
68
+ end
69
+ FeatureGate.from_config_result(res)
70
+ end
72
71
 
73
- res.gate_value
72
+ def get_gate(user, gate_name, options = nil)
73
+ @err_boundary.capture(task: lambda {
74
+ run_with_diagnostics(task: lambda {
75
+ get_gate_impl(user, gate_name,
76
+ disable_log_exposure: options&.disable_log_exposure == true,
77
+ skip_evaluation: options&.skip_evaluation == true,
78
+ disable_evaluation_details: options&.disable_evaluation_details == true
79
+ )
80
+ }, caller: __method__.to_s)
74
81
  }, recover: -> { false }, caller: __method__.to_s)
75
82
  end
76
83
 
77
- sig { params(user: StatsigUser, gate_name: String).void }
78
-
79
- def manually_log_gate_exposure(user, gate_name)
80
- res = @evaluator.check_gate(user, gate_name)
81
- context = {'is_manual_exposure' => true}
82
- @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
84
+ def check_gate(user, gate_name, options = nil)
85
+ @err_boundary.capture(task: lambda {
86
+ run_with_diagnostics(task: lambda {
87
+ get_gate_impl(
88
+ user,
89
+ gate_name,
90
+ disable_log_exposure: options&.disable_log_exposure == true,
91
+ disable_evaluation_details: options&.disable_evaluation_details == true,
92
+ ignore_local_overrides: options&.ignore_local_overrides == true
93
+ ).value
94
+ }, caller: __method__.to_s)
95
+ }, recover: -> { false }, caller: __method__.to_s)
83
96
  end
84
97
 
85
- class GetConfigOptions < T::Struct
86
- prop :log_exposure, T::Boolean, default: true
98
+ def manually_log_gate_exposure(user, gate_name)
99
+ @err_boundary.capture(task: lambda {
100
+ res = Statsig::ConfigResult.new(name: gate_name)
101
+ @evaluator.check_gate(user, gate_name, res)
102
+ context = { :is_manual_exposure => true }
103
+ @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
104
+ })
87
105
  end
88
106
 
89
- sig { params(user: StatsigUser, dynamic_config_name: String, options: GetConfigOptions).returns(DynamicConfig) }
90
-
91
- def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
107
+ def get_config(user, dynamic_config_name, options = nil)
92
108
  @err_boundary.capture(task: lambda {
93
- user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
94
- get_config_impl(user, dynamic_config_name, options)
109
+ run_with_diagnostics(task: lambda {
110
+ user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
111
+ get_config_impl(
112
+ user,
113
+ dynamic_config_name,
114
+ options&.disable_log_exposure == true,
115
+ disable_evaluation_details: options&.disable_evaluation_details == true,
116
+ ignore_local_overrides: options&.ignore_local_overrides == true
117
+ )
118
+ }, caller: __method__.to_s)
95
119
  }, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
96
120
  end
97
121
 
98
- class GetExperimentOptions < T::Struct
99
- prop :log_exposure, T::Boolean, default: true
100
- end
101
-
102
- sig { params(user: StatsigUser, experiment_name: String, options: GetExperimentOptions).returns(DynamicConfig) }
103
-
104
- def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
122
+ def get_experiment(user, experiment_name, options = nil)
105
123
  @err_boundary.capture(task: lambda {
106
- user = verify_inputs(user, experiment_name, "experiment_name")
107
- get_config_impl(user, experiment_name, options)
124
+ run_with_diagnostics(task: lambda {
125
+ user = verify_inputs(user, experiment_name, "experiment_name")
126
+ get_config_impl(
127
+ user,
128
+ experiment_name,
129
+ options&.disable_log_exposure == true,
130
+ user_persisted_values: options&.user_persisted_values,
131
+ disable_evaluation_details: options&.disable_evaluation_details == true,
132
+ ignore_local_overrides: options&.ignore_local_overrides == true
133
+ )
134
+ }, caller: __method__.to_s)
108
135
  }, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
109
136
  end
110
137
 
111
- sig { params(user: StatsigUser, config_name: String).void }
112
-
113
138
  def manually_log_config_exposure(user, config_name)
114
- res = @evaluator.get_config(user, config_name)
115
- context = {'is_manual_exposure' => true}
116
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
117
- end
139
+ @err_boundary.capture(task: lambda {
140
+ res = Statsig::ConfigResult.new(name: config_name)
141
+ @evaluator.get_config(user, config_name, res)
118
142
 
119
- class GetLayerOptions < T::Struct
120
- prop :log_exposure, T::Boolean, default: true
143
+ context = { :is_manual_exposure => true }
144
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
145
+ }, caller: __method__.to_s)
121
146
  end
122
147
 
123
- sig { params(user: StatsigUser, layer_name: String, options: GetLayerOptions).returns(Layer) }
124
-
125
- def get_layer(user, layer_name, options = GetLayerOptions.new)
148
+ def get_user_persisted_values(user, id_type)
126
149
  @err_boundary.capture(task: lambda {
127
- user = verify_inputs(user, layer_name, "layer_name")
150
+ persisted_values = @persistent_storage_utils.get_user_persisted_values(user, id_type)
151
+ return {} if persisted_values.nil?
128
152
 
129
- res = @evaluator.get_layer(user, layer_name)
130
- if res.nil?
131
- res = Statsig::ConfigResult.new(layer_name)
132
- end
133
-
134
- if res == $fetch_from_server
135
- if res.config_delegate.empty?
136
- return Layer.new(layer_name)
137
- end
138
- res = get_config_fallback(user, res.config_delegate)
139
- # exposure logged by the server
140
- end
141
-
142
- exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
143
- @logger.log_layer_exposure(user, layer, parameter_name, res)
144
- } : nil
145
- Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
146
- }, recover: lambda {
147
- Layer.new(layer_name)
153
+ persisted_values
148
154
  }, caller: __method__.to_s)
149
155
  end
150
156
 
151
- sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
157
+ def get_layer(user, layer_name, options = nil)
158
+ @err_boundary.capture(task: lambda {
159
+ run_with_diagnostics(task: lambda {
160
+ user = verify_inputs(user, layer_name, "layer_name")
161
+ exposures_disabled = options&.disable_log_exposure == true
162
+ res = Statsig::ConfigResult.new(
163
+ name: layer_name,
164
+ disable_exposures: exposures_disabled,
165
+ disable_evaluation_details: options&.disable_evaluation_details == true
166
+ )
167
+ @evaluator.get_layer(user, layer_name, res)
168
+
169
+ exposure_log_func = !exposures_disabled ? lambda { |layer, parameter_name|
170
+ @logger.log_layer_exposure(user, layer, parameter_name, res)
171
+ } : nil
172
+
173
+ Layer.new(res.name, res.json_value, res.rule_id, res.group_name, res.config_delegate, exposure_log_func)
174
+ }, caller: __method__.to_s)
175
+ }, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
176
+ end
152
177
 
153
178
  def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
154
- res = @evaluator.get_layer(user, layer_name)
155
- layer = Layer.new(layer_name, res.json_value, res.rule_id)
156
- context = {'is_manual_exposure' => true}
157
- @logger.log_layer_exposure(user, layer, parameter_name, res, context)
179
+ @err_boundary.capture(task: lambda {
180
+ res = Statsig::ConfigResult.new(name: layer_name)
181
+ @evaluator.get_layer(user, layer_name, res)
182
+
183
+ layer = Layer.new(layer_name, res.json_value, res.rule_id, res.group_name, res.config_delegate)
184
+ context = { :is_manual_exposure => true }
185
+ @logger.log_layer_exposure(user, layer, parameter_name, res, context)
186
+ }, caller: __method__.to_s)
158
187
  end
159
188
 
160
189
  def log_event(user, event_name, value = nil, metadata = nil)
@@ -171,7 +200,49 @@ class StatsigDriver
171
200
  event.value = value
172
201
  event.metadata = metadata
173
202
  @logger.log_event(event)
174
- })
203
+ }, caller: __method__.to_s)
204
+ end
205
+
206
+ def manually_sync_rulesets
207
+ @err_boundary.capture(task: lambda {
208
+ @evaluator.spec_store.sync_config_specs
209
+ }, caller: __method__.to_s)
210
+ end
211
+
212
+ def manually_sync_idlists
213
+ @err_boundary.capture(task: lambda {
214
+ @evaluator.spec_store.sync_id_lists
215
+ }, caller: __method__.to_s)
216
+ end
217
+
218
+ def list_gates
219
+ @err_boundary.capture(task: lambda {
220
+ @evaluator.list_gates
221
+ }, caller: __method__.to_s)
222
+ end
223
+
224
+ def list_configs
225
+ @err_boundary.capture(task: lambda {
226
+ @evaluator.list_configs
227
+ }, caller: __method__.to_s)
228
+ end
229
+
230
+ def list_experiments
231
+ @err_boundary.capture(task: lambda {
232
+ @evaluator.list_experiments
233
+ }, caller: __method__.to_s)
234
+ end
235
+
236
+ def list_autotunes
237
+ @err_boundary.capture(task: lambda {
238
+ @evaluator.list_autotunes
239
+ }, caller: __method__.to_s)
240
+ end
241
+
242
+ def list_layers
243
+ @err_boundary.capture(task: lambda {
244
+ @evaluator.list_layers
245
+ }, caller: __method__.to_s)
175
246
  end
176
247
 
177
248
  def shutdown
@@ -179,29 +250,55 @@ class StatsigDriver
179
250
  @shutdown = true
180
251
  @logger.shutdown
181
252
  @evaluator.shutdown
182
- })
253
+ }, caller: __method__.to_s)
183
254
  end
184
255
 
185
256
  def override_gate(gate_name, gate_value)
186
257
  @err_boundary.capture(task: lambda {
187
258
  @evaluator.override_gate(gate_name, gate_value)
188
- })
259
+ }, caller: __method__.to_s)
260
+ end
261
+
262
+ def remove_gate_override(gate_name)
263
+ @err_boundary.capture(task: lambda {
264
+ @evaluator.remove_gate_override(gate_name)
265
+ }, caller: __method__.to_s)
266
+ end
267
+
268
+ def clear_gate_overrides
269
+ @err_boundary.capture(task: lambda {
270
+ @evaluator.clear_gate_overrides
271
+ }, caller: __method__.to_s)
189
272
  end
190
273
 
191
274
  def override_config(config_name, config_value)
192
275
  @err_boundary.capture(task: lambda {
193
276
  @evaluator.override_config(config_name, config_value)
194
- })
277
+ }, caller: __method__.to_s)
278
+ end
279
+
280
+ def remove_config_override(config_name)
281
+ @err_boundary.capture(task: lambda {
282
+ @evaluator.remove_config_override(config_name)
283
+ }, caller: __method__.to_s)
284
+ end
285
+
286
+ def clear_config_overrides
287
+ @err_boundary.capture(task: lambda {
288
+ @evaluator.clear_config_overrides
289
+ }, caller: __method__.to_s)
195
290
  end
196
291
 
197
292
  # @param [StatsigUser] user
293
+ # @param [String | nil] client_sdk_key
294
+ # @param [Boolean] include_local_overrides
198
295
  # @return [Hash]
199
- def get_client_initialize_response(user)
296
+ def get_client_initialize_response(user, hash, client_sdk_key, include_local_overrides)
200
297
  @err_boundary.capture(task: lambda {
201
298
  validate_user(user)
202
299
  normalize_user(user)
203
- @evaluator.get_client_initialize_response(user)
204
- }, recover: -> { nil })
300
+ @evaluator.get_client_initialize_response(user, hash, client_sdk_key, include_local_overrides)
301
+ }, recover: -> { nil }, caller: __method__.to_s)
205
302
  end
206
303
 
207
304
  def maybe_restart_background_threads
@@ -212,12 +309,28 @@ class StatsigDriver
212
309
  @err_boundary.capture(task: lambda {
213
310
  @evaluator.maybe_restart_background_threads
214
311
  @logger.maybe_restart_background_threads
215
- })
312
+ }, caller: __method__.to_s)
216
313
  end
217
314
 
218
315
  private
219
316
 
220
- sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
317
+ def run_with_diagnostics(task:, caller:)
318
+ diagnostics = nil
319
+ if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(1)
320
+ diagnostics = Statsig::Diagnostics.new()
321
+ tracker = diagnostics.track('api_call', caller)
322
+ end
323
+ begin
324
+ res = task.call
325
+ tracker&.end(success: true)
326
+ rescue StandardError => e
327
+ tracker&.end(success: false)
328
+ raise e
329
+ ensure
330
+ @logger.log_diagnostics_event(diagnostics, 'api_call')
331
+ end
332
+ return res
333
+ end
221
334
 
222
335
  def verify_inputs(user, config_name, variable_name)
223
336
  validate_user(user)
@@ -230,22 +343,19 @@ class StatsigDriver
230
343
  normalize_user(user)
231
344
  end
232
345
 
233
- def get_config_impl(user, config_name, options)
234
- res = @evaluator.get_config(user, config_name)
235
- if res.nil?
236
- res = Statsig::ConfigResult.new(config_name)
237
- end
346
+ def get_config_impl(user, config_name, disable_log_exposure, user_persisted_values: nil, disable_evaluation_details: false, ignore_local_overrides: false)
347
+ res = Statsig::ConfigResult.new(
348
+ name: config_name,
349
+ disable_exposures: disable_log_exposure,
350
+ disable_evaluation_details: disable_evaluation_details
351
+ )
352
+ @evaluator.get_config(user, config_name, res, user_persisted_values: user_persisted_values, ignore_local_overrides: ignore_local_overrides)
238
353
 
239
- if res == $fetch_from_server
240
- res = get_config_fallback(user, config_name)
241
- # exposure logged by the server
242
- else
243
- if options.log_exposure
244
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
245
- end
354
+ unless disable_log_exposure
355
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
246
356
  end
247
357
 
248
- DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type)
358
+ DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type, res.evaluation_details)
249
359
  end
250
360
 
251
361
  def validate_user(user)
@@ -261,7 +371,7 @@ class StatsigDriver
261
371
  end
262
372
 
263
373
  def normalize_user(user)
264
- if !@options&.environment.nil?
374
+ if user.statsig_environment.nil? && !@options&.environment.nil?
265
375
  user.statsig_environment = @options.environment
266
376
  end
267
377
  user
@@ -272,34 +382,4 @@ class StatsigDriver
272
382
  puts 'SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
273
383
  end
274
384
  end
275
-
276
- def check_gate_fallback(user, gate_name)
277
- network_result = @net.check_gate(user, gate_name)
278
- if network_result.nil?
279
- config_result = Statsig::ConfigResult.new(gate_name)
280
- return config_result
281
- end
282
-
283
- Statsig::ConfigResult.new(
284
- network_result['name'],
285
- network_result['value'],
286
- {},
287
- network_result['rule_id'],
288
- )
289
- end
290
-
291
- def get_config_fallback(user, dynamic_config_name)
292
- network_result = @net.get_config(user, dynamic_config_name)
293
- if network_result.nil?
294
- config_result = Statsig::ConfigResult.new(dynamic_config_name)
295
- return config_result
296
- end
297
-
298
- Statsig::ConfigResult.new(
299
- network_result['name'],
300
- false,
301
- network_result['value'],
302
- network_result['rule_id'],
303
- )
304
- end
305
385
  end
@@ -1,3 +1,4 @@
1
+
1
2
  module Statsig
2
3
  class UninitializedError < StandardError
3
4
  def initialize(msg="Must call initialize first.")
@@ -8,4 +9,10 @@ module Statsig
8
9
  class ValueError < StandardError
9
10
 
10
11
  end
12
+
13
+ class InvalidSDKKeyResponse < StandardError
14
+ def initialize(msg="Incorrect SDK Key used to generate response.")
15
+ super
16
+ end
17
+ end
11
18
  end
data/lib/statsig_event.rb CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
1
+
2
2
  class StatsigEvent
3
3
  attr_accessor :value, :metadata, :statsig_metadata, :secondary_exposures
4
4
  attr_reader :user
@@ -21,13 +21,13 @@ class StatsigEvent
21
21
 
22
22
  def serialize
23
23
  {
24
- 'eventName' => @event_name,
25
- 'metadata' => @metadata,
26
- 'value' => @value,
27
- 'user' => @user,
28
- 'time' => @time,
29
- 'statsigMetadata' => @statsig_metadata,
30
- 'secondaryExposures' => @secondary_exposures
24
+ :eventName => @event_name,
25
+ :metadata => @metadata,
26
+ :value => @value,
27
+ :user => @user,
28
+ :time => @time,
29
+ :statsigMetadata => @statsig_metadata,
30
+ :secondaryExposures => @secondary_exposures
31
31
  }
32
32
  end
33
33
  end