statsig 1.25.1 → 1.26.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.
@@ -30,19 +30,19 @@ class StatsigDriver
30
30
  end
31
31
 
32
32
  @err_boundary = Statsig::ErrorBoundary.new(secret_key)
33
- @err_boundary.capture(-> {
34
- @init_diagnostics = Statsig::Diagnostics.new("initialize")
35
- @init_diagnostics.mark("overall", "start")
33
+ @err_boundary.capture(task: lambda {
34
+ @diagnostics = Statsig::Diagnostics.new('initialize')
35
+ tracker = @diagnostics.track('overall')
36
36
  @options = options || StatsigOptions.new
37
37
  @shutdown = false
38
38
  @secret_key = secret_key
39
39
  @net = Statsig::Network.new(secret_key, @options)
40
- @logger = Statsig::StatsigLogger.new(@net, @options)
41
- @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @init_diagnostics)
42
- @init_diagnostics.mark("overall", "end")
40
+ @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
41
+ @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger)
42
+ tracker.end(success: true)
43
43
 
44
- log_init_diagnostics
45
- })
44
+ @logger.log_diagnostics_event(@diagnostics)
45
+ }, caller: __method__.to_s)
46
46
  end
47
47
 
48
48
  class CheckGateOptions < T::Struct
@@ -52,34 +52,37 @@ class StatsigDriver
52
52
  sig { params(user: StatsigUser, gate_name: String, options: CheckGateOptions).returns(T::Boolean) }
53
53
 
54
54
  def check_gate(user, gate_name, options = CheckGateOptions.new)
55
- @err_boundary.capture(-> {
56
- user = verify_inputs(user, gate_name, "gate_name")
57
-
58
- res = @evaluator.check_gate(user, gate_name)
59
- if res.nil?
60
- res = Statsig::ConfigResult.new(gate_name)
61
- end
55
+ @err_boundary.capture(task: lambda {
56
+ run_with_diagnostics(task: lambda {
57
+ user = verify_inputs(user, gate_name, "gate_name")
62
58
 
63
- if res == $fetch_from_server
64
- res = check_gate_fallback(user, gate_name)
65
- # exposure logged by the server
66
- else
67
- if options.log_exposure
68
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
59
+ res = @evaluator.check_gate(user, gate_name)
60
+ if res.nil?
61
+ res = Statsig::ConfigResult.new(gate_name)
69
62
  end
70
- end
71
63
 
72
- res.gate_value
73
- }, -> { false })
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
74
72
 
73
+ res.gate_value
74
+ }, caller: __method__.to_s)
75
+ }, recover: -> { false }, caller: __method__.to_s)
75
76
  end
76
77
 
77
78
  sig { params(user: StatsigUser, gate_name: String).void }
78
79
 
79
80
  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)
81
+ @err_boundary.capture(task: lambda {
82
+ res = @evaluator.check_gate(user, gate_name)
83
+ context = { 'is_manual_exposure' => true }
84
+ @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
85
+ })
83
86
  end
84
87
 
85
88
  class GetConfigOptions < T::Struct
@@ -89,10 +92,12 @@ class StatsigDriver
89
92
  sig { params(user: StatsigUser, dynamic_config_name: String, options: GetConfigOptions).returns(DynamicConfig) }
90
93
 
91
94
  def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
92
- @err_boundary.capture(-> {
93
- user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
94
- get_config_impl(user, dynamic_config_name, options)
95
- }, -> { DynamicConfig.new(dynamic_config_name) })
95
+ @err_boundary.capture(task: lambda {
96
+ run_with_diagnostics(task: lambda {
97
+ user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
98
+ get_config_impl(user, dynamic_config_name, options)
99
+ }, caller: __method__.to_s)
100
+ }, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
96
101
  end
97
102
 
98
103
  class GetExperimentOptions < T::Struct
@@ -102,18 +107,22 @@ class StatsigDriver
102
107
  sig { params(user: StatsigUser, experiment_name: String, options: GetExperimentOptions).returns(DynamicConfig) }
103
108
 
104
109
  def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
105
- @err_boundary.capture(-> {
106
- user = verify_inputs(user, experiment_name, "experiment_name")
107
- get_config_impl(user, experiment_name, options)
108
- }, -> { DynamicConfig.new(experiment_name) })
110
+ @err_boundary.capture(task: lambda {
111
+ run_with_diagnostics(task: lambda {
112
+ user = verify_inputs(user, experiment_name, "experiment_name")
113
+ get_config_impl(user, experiment_name, options)
114
+ }, caller: __method__.to_s)
115
+ }, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
109
116
  end
110
117
 
111
118
  sig { params(user: StatsigUser, config_name: String).void }
112
119
 
113
120
  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)
121
+ @err_boundary.capture(task: lambda {
122
+ res = @evaluator.get_config(user, config_name)
123
+ context = { 'is_manual_exposure' => true }
124
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
125
+ }, caller: __method__.to_s)
117
126
  end
118
127
 
119
128
  class GetLayerOptions < T::Struct
@@ -123,42 +132,44 @@ class StatsigDriver
123
132
  sig { params(user: StatsigUser, layer_name: String, options: GetLayerOptions).returns(Layer) }
124
133
 
125
134
  def get_layer(user, layer_name, options = GetLayerOptions.new)
126
- @err_boundary.capture(-> {
127
- user = verify_inputs(user, layer_name, "layer_name")
135
+ @err_boundary.capture(task: lambda {
136
+ run_with_diagnostics(task: lambda {
137
+ user = verify_inputs(user, layer_name, "layer_name")
128
138
 
129
- res = @evaluator.get_layer(user, layer_name)
130
- if res.nil?
131
- res = Statsig::ConfigResult.new(layer_name)
132
- end
139
+ res = @evaluator.get_layer(user, layer_name)
140
+ if res.nil?
141
+ res = Statsig::ConfigResult.new(layer_name)
142
+ end
133
143
 
134
- if res == $fetch_from_server
135
- if res.config_delegate.empty?
136
- return Layer.new(layer_name)
144
+ if res == $fetch_from_server
145
+ if res.config_delegate.empty?
146
+ return Layer.new(layer_name)
147
+ end
148
+ res = get_config_fallback(user, res.config_delegate)
149
+ # exposure logged by the server
137
150
  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
- }, -> {
147
- Layer.new(layer_name)
148
- })
151
+
152
+ exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
153
+ @logger.log_layer_exposure(user, layer, parameter_name, res)
154
+ } : nil
155
+ Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
156
+ }, caller: __method__.to_s)
157
+ }, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
149
158
  end
150
159
 
151
160
  sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
152
161
 
153
162
  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)
163
+ @err_boundary.capture(task: lambda {
164
+ res = @evaluator.get_layer(user, layer_name)
165
+ layer = Layer.new(layer_name, res.json_value, res.rule_id)
166
+ context = { 'is_manual_exposure' => true }
167
+ @logger.log_layer_exposure(user, layer, parameter_name, res, context)
168
+ }, caller: __method__.to_s)
158
169
  end
159
170
 
160
171
  def log_event(user, event_name, value = nil, metadata = nil)
161
- @err_boundary.capture(-> {
172
+ @err_boundary.capture(task: lambda {
162
173
  if !user.nil? && !user.instance_of?(StatsigUser)
163
174
  raise Statsig::ValueError.new('Must provide a valid StatsigUser or nil')
164
175
  end
@@ -171,37 +182,38 @@ class StatsigDriver
171
182
  event.value = value
172
183
  event.metadata = metadata
173
184
  @logger.log_event(event)
174
- })
185
+ }, caller: __method__.to_s)
175
186
  end
176
187
 
177
188
  def shutdown
178
- @err_boundary.capture(-> {
189
+ @err_boundary.capture(task: lambda {
179
190
  @shutdown = true
180
191
  @logger.shutdown
181
192
  @evaluator.shutdown
182
- })
193
+ }, caller: __method__.to_s)
183
194
  end
184
195
 
185
196
  def override_gate(gate_name, gate_value)
186
- @err_boundary.capture(-> {
197
+ @err_boundary.capture(task: lambda {
187
198
  @evaluator.override_gate(gate_name, gate_value)
188
- })
199
+ }, caller: __method__.to_s)
189
200
  end
190
201
 
191
202
  def override_config(config_name, config_value)
192
- @err_boundary.capture(-> {
203
+ @err_boundary.capture(task: lambda {
193
204
  @evaluator.override_config(config_name, config_value)
194
- })
205
+ }, caller: __method__.to_s)
195
206
  end
196
207
 
197
208
  # @param [StatsigUser] user
209
+ # @param [String | nil] client_sdk_key
198
210
  # @return [Hash]
199
- def get_client_initialize_response(user)
200
- @err_boundary.capture(-> {
211
+ def get_client_initialize_response(user, hash, client_sdk_key)
212
+ @err_boundary.capture(task: lambda {
201
213
  validate_user(user)
202
214
  normalize_user(user)
203
- @evaluator.get_client_initialize_response(user)
204
- }, -> { nil })
215
+ @evaluator.get_client_initialize_response(user, hash, client_sdk_key)
216
+ }, recover: -> { nil }, caller: __method__.to_s)
205
217
  end
206
218
 
207
219
  def maybe_restart_background_threads
@@ -209,14 +221,32 @@ class StatsigDriver
209
221
  return
210
222
  end
211
223
 
212
- @err_boundary.capture(-> {
224
+ @err_boundary.capture(task: lambda {
213
225
  @evaluator.maybe_restart_background_threads
214
226
  @logger.maybe_restart_background_threads
215
- })
227
+ }, caller: __method__.to_s)
216
228
  end
217
229
 
218
230
  private
219
231
 
232
+ def run_with_diagnostics(task:, caller:)
233
+ diagnostics = nil
234
+ if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(10_000)
235
+ diagnostics = Statsig::Diagnostics.new('api_call')
236
+ tracker = diagnostics.track(caller)
237
+ end
238
+ begin
239
+ res = task.call
240
+ tracker&.end(success: true)
241
+ rescue StandardError => e
242
+ tracker&.end(success: false)
243
+ raise e
244
+ ensure
245
+ @logger.log_diagnostics_event(diagnostics)
246
+ end
247
+ return res
248
+ end
249
+
220
250
  sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
221
251
 
222
252
  def verify_inputs(user, config_name, variable_name)
@@ -302,12 +332,4 @@ class StatsigDriver
302
332
  network_result['rule_id'],
303
333
  )
304
334
  end
305
-
306
- def log_init_diagnostics
307
- if @options.disable_diagnostics_logging
308
- return
309
- end
310
-
311
- @logger.log_diagnostics_event(@init_diagnostics)
312
- end
313
335
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Statsig
2
3
  class UninitializedError < StandardError
3
4
  def initialize(msg="Must call initialize first.")
@@ -9,7 +9,7 @@ $diagnostics_event = 'statsig::diagnostics'
9
9
  $ignored_metadata_keys = ['serverTime', 'configSyncTime', 'initTime', 'reason']
10
10
  module Statsig
11
11
  class StatsigLogger
12
- def initialize(network, options)
12
+ def initialize(network, options, error_boundary)
13
13
  @network = network
14
14
  @events = []
15
15
  @options = options
@@ -23,9 +23,11 @@ module Statsig
23
23
  fallback_policy: :discard
24
24
  )
25
25
 
26
+ @error_boundary = error_boundary
26
27
  @background_flush = periodic_flush
27
28
  @deduper = Concurrent::Set.new()
28
29
  @interval = 0
30
+ @flush_mutex = Mutex.new
29
31
  end
30
32
 
31
33
  def log_event(event)
@@ -97,20 +99,26 @@ module Statsig
97
99
  end
98
100
 
99
101
  def log_diagnostics_event(diagnostics, user = nil)
102
+ return if @options.disable_diagnostics_logging
103
+ return if diagnostics.nil? || diagnostics.markers.empty?
104
+
100
105
  event = StatsigEvent.new($diagnostics_event)
101
106
  event.user = user
102
107
  event.metadata = diagnostics.serialize
103
108
  log_event(event)
109
+ diagnostics.clear_markers
104
110
  end
105
111
 
106
112
  def periodic_flush
107
113
  Thread.new do
108
- loop do
109
- sleep @options.logging_interval_seconds
110
- flush_async
111
- @interval += 1
112
- @deduper.clear if @interval % 2 == 0
113
- end
114
+ @error_boundary.capture(task: lambda {
115
+ loop do
116
+ sleep @options.logging_interval_seconds
117
+ flush_async
118
+ @interval += 1
119
+ @deduper.clear if @interval % 2 == 0
120
+ end
121
+ })
114
122
  end
115
123
  end
116
124
 
@@ -128,18 +136,20 @@ module Statsig
128
136
  end
129
137
 
130
138
  def flush
131
- if @events.length == 0
132
- return
133
- end
134
- events_clone = @events
135
- @events = []
136
- flush_events = events_clone.map { |e| e.serialize }
139
+ @flush_mutex.synchronize do
140
+ if @events.length.zero?
141
+ return
142
+ end
137
143
 
138
- @network.post_logs(flush_events)
144
+ events_clone = @events
145
+ @events = []
146
+ flush_events = events_clone.map { |e| e.serialize }
147
+ @network.post_logs(flush_events)
148
+ end
139
149
  end
140
150
 
141
151
  def maybe_restart_background_threads
142
- if @background_flush.nil? or !@background_flush.alive?
152
+ if @background_flush.nil? || !@background_flush.alive?
143
153
  @background_flush = periodic_flush
144
154
  end
145
155
  end
@@ -19,6 +19,11 @@ class StatsigOptions
19
19
  # default: https://statsigapi.net/v1
20
20
  attr_accessor :api_url_base
21
21
 
22
+ # The base url used specifically to call download_config_specs.
23
+ # Takes precedence over api_url_base
24
+ sig { returns(T.any(String, NilClass)) }
25
+ attr_accessor :api_url_download_config_specs
26
+
22
27
  sig { returns(T.any(Float, Integer)) }
23
28
  # The interval (in seconds) to poll for changes to your Statsig configuration
24
29
  # default: 10s
@@ -97,6 +102,7 @@ class StatsigOptions
97
102
  params(
98
103
  environment: T.any(T::Hash[String, String], NilClass),
99
104
  api_url_base: String,
105
+ api_url_download_config_specs: T.any(String, NilClass),
100
106
  rulesets_sync_interval: T.any(Float, Integer),
101
107
  idlists_sync_interval: T.any(Float, Integer),
102
108
  logging_interval_seconds: T.any(Float, Integer),
@@ -118,6 +124,7 @@ class StatsigOptions
118
124
  def initialize(
119
125
  environment = nil,
120
126
  api_url_base = 'https://statsigapi.net/v1',
127
+ api_url_download_config_specs: nil,
121
128
  rulesets_sync_interval: 10,
122
129
  idlists_sync_interval: 60,
123
130
  logging_interval_seconds: 60,
@@ -135,6 +142,7 @@ class StatsigOptions
135
142
  post_logs_retry_backoff: nil)
136
143
  @environment = environment.is_a?(Hash) ? environment : nil
137
144
  @api_url_base = api_url_base
145
+ @api_url_download_config_specs = api_url_download_config_specs
138
146
  @rulesets_sync_interval = rulesets_sync_interval
139
147
  @idlists_sync_interval = idlists_sync_interval
140
148
  @logging_interval_seconds = logging_interval_seconds
data/lib/ua_parser.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'user_agent_parser'
2
3
 
3
4
  module UAParser
data/lib/uri_helper.rb ADDED
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ class URIHelper
6
+ class URIBuilder
7
+ extend T::Sig
8
+
9
+ sig { returns(StatsigOptions) }
10
+ attr_accessor :options
11
+
12
+ sig { params(options: StatsigOptions).void }
13
+ def initialize(options)
14
+ @options = options
15
+ end
16
+
17
+ sig { params(endpoint: String).returns(String) }
18
+ def build_url(endpoint)
19
+ api = @options.api_url_base
20
+ if endpoint == 'download_config_specs' && !@options.api_url_download_config_specs.nil?
21
+ api = T.must(@options.api_url_download_config_specs)
22
+ end
23
+ unless api.end_with?('/')
24
+ api += '/'
25
+ end
26
+ "#{api}#{endpoint}"
27
+ end
28
+ end
29
+
30
+ def self.initialize(options)
31
+ @uri_builder = URIBuilder.new(options)
32
+ end
33
+
34
+ def self.build_url(endpoint)
35
+ @uri_builder.build_url(endpoint)
36
+ end
37
+ 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.25.1
4
+ version: 1.26.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-05-31 00:00:00.000000000 Z
11
+ date: 2023-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 5.14.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-suite
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.0.3
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.0.3
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: spy
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -226,6 +254,26 @@ dependencies:
226
254
  - - "<"
227
255
  - !ruby/object:Gem::Version
228
256
  version: '6.0'
257
+ - !ruby/object:Gem::Dependency
258
+ name: connection_pool
259
+ requirement: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - "~>"
262
+ - !ruby/object:Gem::Version
263
+ version: '2.4'
264
+ - - ">="
265
+ - !ruby/object:Gem::Version
266
+ version: 2.4.1
267
+ type: :runtime
268
+ prerelease: false
269
+ version_requirements: !ruby/object:Gem::Requirement
270
+ requirements:
271
+ - - "~>"
272
+ - !ruby/object:Gem::Version
273
+ version: '2.4'
274
+ - - ">="
275
+ - !ruby/object:Gem::Version
276
+ version: 2.4.1
229
277
  - !ruby/object:Gem::Dependency
230
278
  name: ip3country
231
279
  requirement: !ruby/object:Gem::Requirement
@@ -282,6 +330,7 @@ files:
282
330
  - lib/evaluation_details.rb
283
331
  - lib/evaluation_helpers.rb
284
332
  - lib/evaluator.rb
333
+ - lib/hash_utils.rb
285
334
  - lib/id_list.rb
286
335
  - lib/interfaces/data_store.rb
287
336
  - lib/layer.rb
@@ -295,6 +344,7 @@ files:
295
344
  - lib/statsig_options.rb
296
345
  - lib/statsig_user.rb
297
346
  - lib/ua_parser.rb
347
+ - lib/uri_helper.rb
298
348
  homepage: https://rubygems.org/gems/statsig
299
349
  licenses:
300
350
  - ISC