statsig 1.17.0 → 1.18.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: 5b81f929cdedefaee6f57cb2b0f7aec05bbfd28cf6b4f273fc4fb3bbcf46858f
4
- data.tar.gz: 5caa719d8a301cbcd95d68f9bc5802a89f0691600c7b5883bfdbfcc8fa21bb8f
3
+ metadata.gz: 475c2e56b1f53dbc642adff47dd6a1de3a3c324ce9a0145fcaf7c6ebb2b412c1
4
+ data.tar.gz: a37bbef25c2ce1b818fafe25252bcf076964b16acdb484d41ebe2c705b6042b1
5
5
  SHA512:
6
- metadata.gz: 9372cd1b25a0b91054d6315df027833cd2377e51e1f76385dd260fe9a17665da33330c357cf63756f6bc6701e51f76a7d5b81aa99d57a96bfeee807c575c3b19
7
- data.tar.gz: 5dd1c6ad849e20c0ed15b451371a78e971f6da4ed83f7bf7f79eb5db65c88ba27f1b8e9da991587d8a46cbd2086d0b30692bbb81e48283e758b84d679a63eb3a
6
+ metadata.gz: 32ba1629776babb0f4d437e551a251e5bea38b42b916c067c0009705ef59b71db5989a6942e009994ab5956e10ca61cf7ee4a14f9302312094e2b2cc479568c5
7
+ data.tar.gz: d8d6f082a647a6b6ba7d6adf8954c17d845b44b55dfd7b17ce4fff0aa858cbce96d6308e2ec0e0f15209275d5e8520f0b5697d1caf64cdac7e40e0d7dc46b4da
@@ -0,0 +1,44 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module Statsig
6
+ class Diagnostics
7
+ extend T::Sig
8
+
9
+ sig { returns(String) }
10
+ attr_reader :context
11
+
12
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
13
+ attr_reader :markers
14
+
15
+ sig { params(context: String).void }
16
+
17
+ def initialize(context)
18
+ @context = context
19
+ @markers = []
20
+ end
21
+
22
+ sig { params(key: String, action: String, step: T.any(String, NilClass), value: T.any(String, Integer, T::Boolean, NilClass)).void }
23
+
24
+ def mark(key, action, step = nil, value = nil)
25
+ @markers.push({
26
+ key: key,
27
+ step: step,
28
+ action: action,
29
+ value: value,
30
+ timestamp: (Time.now.to_f * 1000).to_i
31
+ })
32
+ end
33
+
34
+ sig { returns(T::Hash[Symbol, T.untyped]) }
35
+
36
+ def serialize
37
+ {
38
+ context: @context,
39
+ markers: @markers
40
+ }
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,57 @@
1
+ require "statsig_errors"
2
+
3
+ $endpoint = 'https://statsigapi.net/v1/sdk_exception'
4
+
5
+ module Statsig
6
+ class ErrorBoundary
7
+ def initialize(sdk_key)
8
+ @sdk_key = sdk_key
9
+ @seen = Set.new
10
+ end
11
+
12
+ def capture(task, recover = -> {})
13
+ begin
14
+ return task.call
15
+ rescue StandardError => e
16
+ if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
17
+ raise e
18
+ end
19
+ puts "[Statsig]: An unexpected exception occurred."
20
+ log_exception(e)
21
+ return recover.call
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def log_exception(exception)
28
+ begin
29
+ name = exception.class.name
30
+ if @seen.include?(name)
31
+ return
32
+ end
33
+
34
+ @seen << name
35
+ meta = Statsig.get_statsig_metadata
36
+ http = HTTP.headers(
37
+ {
38
+ "STATSIG-API-KEY" => @sdk_key,
39
+ "STATSIG-SDK-TYPE" => meta['sdkType'],
40
+ "STATSIG-SDK-VERSION" => meta['sdkVersion'],
41
+ "Content-Type" => "application/json; charset=UTF-8"
42
+ }).accept(:json)
43
+ body = {
44
+ "exception" => name,
45
+ "info" => {
46
+ "trace" => exception.backtrace.to_s,
47
+ "message" => exception.message
48
+ }.to_s,
49
+ "statsigMetadata" => meta
50
+ }
51
+ http.post($endpoint, body: JSON.generate(body))
52
+ rescue
53
+ return
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/evaluator.rb CHANGED
@@ -17,8 +17,8 @@ module Statsig
17
17
  class Evaluator
18
18
  attr_accessor :spec_store
19
19
 
20
- def initialize(network, options, error_callback)
21
- @spec_store = Statsig::SpecStore.new(network, options, error_callback)
20
+ def initialize(network, options, error_callback, init_diagnostics = nil)
21
+ @spec_store = Statsig::SpecStore.new(network, options, error_callback, init_diagnostics)
22
22
  @ua_parser = UserAgentParser::Parser.new
23
23
  CountryLookup.initialize
24
24
 
data/lib/network.rb CHANGED
@@ -8,6 +8,15 @@ require 'sorbet-runtime'
8
8
  $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
9
9
 
10
10
  module Statsig
11
+ class NetworkError < StandardError
12
+ attr_reader :http_code
13
+
14
+ def initialize(msg = nil, http_code = nil)
15
+ super(msg)
16
+ @http_code = http_code
17
+ end
18
+ end
19
+
11
20
  class Network
12
21
  extend T::Sig
13
22
 
@@ -25,19 +34,23 @@ module Statsig
25
34
  @session_id = SecureRandom.uuid
26
35
  end
27
36
 
28
-
29
37
  sig { params(endpoint: String, body: String, retries: Integer, backoff: Integer)
30
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)]) }
38
+ .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)]) }
31
39
 
32
40
  def post_helper(endpoint, body, retries = 0, backoff = 1)
33
41
  if @local_mode
34
42
  return nil, nil
35
43
  end
44
+
45
+ meta = Statsig.get_statsig_metadata
36
46
  http = HTTP.headers(
37
- {"STATSIG-API-KEY" => @server_secret,
38
- "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_i.to_s,
39
- "STATSIG-SERVER-SESSION-ID" => @session_id,
40
- "Content-Type" => "application/json; charset=UTF-8"
47
+ {
48
+ "STATSIG-API-KEY" => @server_secret,
49
+ "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_i.to_s,
50
+ "STATSIG-SERVER-SESSION-ID" => @session_id,
51
+ "Content-Type" => "application/json; charset=UTF-8",
52
+ "STATSIG-SDK-TYPE" => meta['sdkType'],
53
+ "STATSIG-SDK-VERSION" => meta['sdkVersion'],
41
54
  }).accept(:json)
42
55
  begin
43
56
  res = http.post(@api + endpoint, body: body)
@@ -47,8 +60,8 @@ module Statsig
47
60
  sleep backoff
48
61
  return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
49
62
  end
50
- return res, nil unless !res.status.success?
51
- return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
63
+ return res, nil if res.status.success?
64
+ return nil, NetworkError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
52
65
  ## status code retry
53
66
  sleep backoff
54
67
  post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
@@ -56,7 +69,7 @@ module Statsig
56
69
 
57
70
  def check_gate(user, gate_name)
58
71
  begin
59
- request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
72
+ request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
60
73
  response, _ = post_helper('check_gate', request_body)
61
74
  return JSON.parse(response.body) unless response.nil?
62
75
  false
@@ -67,7 +80,7 @@ module Statsig
67
80
 
68
81
  def get_config(user, dynamic_config_name)
69
82
  begin
70
- request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
83
+ request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
71
84
  response, _ = post_helper('get_config', request_body)
72
85
  return JSON.parse(response.body) unless response.nil?
73
86
  nil
@@ -78,7 +91,7 @@ module Statsig
78
91
 
79
92
  def post_logs(events)
80
93
  begin
81
- json_body = JSON.generate({'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata})
94
+ json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
82
95
  post_helper('log_event', json_body, 5)
83
96
  rescue
84
97
  end
data/lib/spec_store.rb CHANGED
@@ -14,7 +14,7 @@ module Statsig
14
14
  attr_accessor :initial_config_sync_time
15
15
  attr_accessor :init_reason
16
16
 
17
- def initialize(network, options, error_callback)
17
+ def initialize(network, options, error_callback, init_diagnostics = nil)
18
18
  @init_reason = EvaluationReason::UNINITIALIZED
19
19
  @network = network
20
20
  @options = options
@@ -42,8 +42,12 @@ module Statsig
42
42
  begin
43
43
  if !@options.data_store.nil?
44
44
  puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
45
- elsif process(options.bootstrap_values)
46
- @init_reason = EvaluationReason::BOOTSTRAP
45
+ else
46
+ init_diagnostics&.mark("bootstrap", "start", "load")
47
+ if process(options.bootstrap_values)
48
+ @init_reason = EvaluationReason::BOOTSTRAP
49
+ end
50
+ init_diagnostics&.mark("bootstrap", "end", "load", @init_reason == EvaluationReason::BOOTSTRAP)
47
51
  end
48
52
  rescue
49
53
  puts 'the provided bootstrapValues is not a valid JSON string'
@@ -51,13 +55,18 @@ module Statsig
51
55
  end
52
56
 
53
57
  unless @options.data_store.nil?
58
+ init_diagnostics&.mark("data_store", "start", "load")
54
59
  @options.data_store.init
55
60
  load_from_storage_adapter
61
+ init_diagnostics&.mark("data_store", "end", "load", @init_reason == EvaluationReason::DATA_ADAPTER)
62
+ end
63
+
64
+ if @init_reason == EvaluationReason::UNINITIALIZED
65
+ download_config_specs(init_diagnostics)
56
66
  end
57
67
 
58
- download_config_specs
59
68
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
60
- get_id_lists
69
+ get_id_lists(init_diagnostics)
61
70
 
62
71
  @config_sync_thread = sync_config_specs
63
72
  @id_lists_sync_thread = sync_id_lists
@@ -157,26 +166,39 @@ module Statsig
157
166
  end
158
167
  end
159
168
 
160
- def download_config_specs
161
- e = get_config_specs_from_network
162
- @error_callback.call(e) unless e.nil? or @error_callback.nil?
163
- end
169
+ def download_config_specs(init_diagnostics = nil)
170
+ init_diagnostics&.mark("download_config_specs", "start", "network_request")
164
171
 
165
- def get_config_specs_from_network
172
+ error = nil
166
173
  begin
167
174
  response, e = @network.post_helper('download_config_specs', JSON.generate({ 'sinceTime' => @last_config_sync_time }))
175
+ code = response&.status.to_i
176
+ if e.is_a? NetworkError
177
+ code = e.http_code
178
+ end
179
+ init_diagnostics&.mark("download_config_specs", "end", "network_request", code)
180
+
168
181
  if e.nil?
169
- if !response.nil? and process(response.body)
170
- @init_reason = EvaluationReason::NETWORK
171
- @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
182
+ unless response.nil?
183
+ init_diagnostics&.mark("download_config_specs", "start", "process")
184
+
185
+ if process(response.body)
186
+ @init_reason = EvaluationReason::NETWORK
187
+ @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
188
+ end
189
+
190
+ init_diagnostics&.mark("download_config_specs", "end", "process", @init_reason == EvaluationReason::NETWORK)
172
191
  end
192
+
173
193
  nil
174
194
  else
175
- e
195
+ error = e
176
196
  end
177
197
  rescue StandardError => e
178
- e
198
+ error = e
179
199
  end
200
+
201
+ @error_callback.call(error) unless error.nil? or @error_callback.nil?
180
202
  end
181
203
 
182
204
  def process(specs_string, from_adapter = false)
@@ -219,11 +241,13 @@ module Statsig
219
241
  true
220
242
  end
221
243
 
222
- def get_id_lists
244
+ def get_id_lists(init_diagnostics = nil)
245
+ init_diagnostics&.mark("get_id_lists", "start", "network_request")
223
246
  response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
224
247
  if !e.nil? || response.nil?
225
248
  return
226
249
  end
250
+ init_diagnostics&.mark("get_id_lists", "end", "network_request", response.status.to_i)
227
251
 
228
252
  begin
229
253
  server_id_lists = JSON.parse(response)
@@ -233,6 +257,12 @@ module Statsig
233
257
  end
234
258
  tasks = []
235
259
 
260
+ if server_id_lists.length == 0
261
+ return
262
+ end
263
+
264
+ init_diagnostics&.mark("get_id_lists", "start", "process", server_id_lists.length)
265
+
236
266
  server_id_lists.each do |list_name, list|
237
267
  server_list = IDList.new(list)
238
268
  local_list = get_id_list(list_name)
@@ -267,6 +297,7 @@ module Statsig
267
297
 
268
298
  result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
269
299
  if result.state != :fulfilled
300
+ init_diagnostics&.mark("get_id_lists", "end", "process", false)
270
301
  return # timed out
271
302
  end
272
303
 
@@ -279,6 +310,7 @@ module Statsig
279
310
  delete_lists.each do |list_name|
280
311
  local_id_lists.delete list_name
281
312
  end
313
+ init_diagnostics&.mark("get_id_lists", "end", "process", true)
282
314
  rescue
283
315
  # Ignored, will try again
284
316
  end
data/lib/statsig.rb CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  require 'statsig_driver'
4
4
  require 'sorbet-runtime'
5
+ require 'statsig_errors'
5
6
 
6
7
  module Statsig
7
8
  extend T::Sig
8
9
 
9
-
10
10
  sig { params(secret_key: String, options: T.any(StatsigOptions, NilClass), error_callback: T.any(Method, Proc, NilClass)).void }
11
11
  ##
12
12
  # Initializes the Statsig SDK.
@@ -89,7 +89,7 @@ module Statsig
89
89
  ##
90
90
  # Stops all Statsig activity and flushes any pending events.
91
91
  def self.shutdown
92
- unless @shared_instance.nil?
92
+ if defined? @shared_instance and !@shared_instance.nil?
93
93
  @shared_instance.shutdown
94
94
  end
95
95
  @shared_instance = nil
@@ -136,32 +136,32 @@ module Statsig
136
136
  def self.get_statsig_metadata
137
137
  {
138
138
  'sdkType' => 'ruby-server',
139
- 'sdkVersion' => '1.17.0',
139
+ 'sdkVersion' => '1.18.0',
140
140
  }
141
141
  end
142
142
 
143
143
  private
144
144
 
145
145
  def self.ensure_initialized
146
- if @shared_instance.nil?
147
- raise 'Must call initialize first.'
146
+ if not defined? @shared_instance or @shared_instance.nil?
147
+ raise Statsig::UninitializedError.new
148
148
  end
149
149
  end
150
150
 
151
151
  T::Configuration.call_validation_error_handler = lambda do |signature, opts|
152
- puts opts[:pretty_message]
152
+ puts "[Type Error] " + opts[:pretty_message]
153
153
  end
154
154
 
155
155
  T::Configuration.inline_type_error_handler = lambda do |error, opts|
156
- puts error.message
156
+ puts "[Type Error] " + error.message
157
157
  end
158
158
 
159
159
  T::Configuration.sig_builder_error_handler = lambda do |error, location|
160
- puts error.message
160
+ puts "[Type Error] " + error.message
161
161
  end
162
162
 
163
163
  T::Configuration.sig_validation_error_handler = lambda do |error, opts|
164
- puts error.message
164
+ puts "[Type Error] " + error.message
165
165
  end
166
166
 
167
167
  end
@@ -3,14 +3,17 @@
3
3
  require 'config_result'
4
4
  require 'evaluator'
5
5
  require 'network'
6
+ require 'statsig_errors'
6
7
  require 'statsig_event'
7
8
  require 'statsig_logger'
8
9
  require 'statsig_options'
9
10
  require 'statsig_user'
10
11
  require 'spec_store'
11
12
  require 'dynamic_config'
13
+ require 'error_boundary'
12
14
  require 'layer'
13
15
  require 'sorbet-runtime'
16
+ require 'diagnostics'
14
17
 
15
18
  class StatsigDriver
16
19
  extend T::Sig
@@ -19,128 +22,167 @@ class StatsigDriver
19
22
 
20
23
  def initialize(secret_key, options = nil, error_callback = nil)
21
24
  unless secret_key.start_with?('secret-')
22
- raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
25
+ raise Statsig::ValueError.new('Invalid secret key provided. Provide your project secret key from the Statsig console')
23
26
  end
27
+
24
28
  if !options.nil? && !options.instance_of?(StatsigOptions)
25
- raise 'Invalid options provided. Either provide a valid StatsigOptions object or nil'
29
+ raise Statsig::ValueError.new('Invalid options provided. Either provide a valid StatsigOptions object or nil')
26
30
  end
27
31
 
28
- @options = options || StatsigOptions.new
29
- @shutdown = false
30
- @secret_key = secret_key
31
- @net = Statsig::Network.new(secret_key, @options.api_url_base, @options.local_mode)
32
- @logger = Statsig::StatsigLogger.new(@net, @options)
33
- @evaluator = Statsig::Evaluator.new(@net, @options, error_callback)
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")
36
+ @options = options || StatsigOptions.new
37
+ @shutdown = false
38
+ @secret_key = secret_key
39
+ @net = Statsig::Network.new(secret_key, @options.api_url_base, @options.local_mode)
40
+ @logger = Statsig::StatsigLogger.new(@net, @options)
41
+ @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @init_diagnostics)
42
+ @init_diagnostics.mark("overall", "end")
43
+
44
+ log_init_diagnostics
45
+ })
34
46
  end
35
47
 
36
48
  sig { params(user: StatsigUser, gate_name: String).returns(T::Boolean) }
37
49
 
38
50
  def check_gate(user, gate_name)
39
- user = verify_inputs(user, gate_name, "gate_name")
51
+ @err_boundary.capture(-> {
52
+ user = verify_inputs(user, gate_name, "gate_name")
40
53
 
41
- res = @evaluator.check_gate(user, gate_name)
42
- if res.nil?
43
- res = Statsig::ConfigResult.new(gate_name)
44
- end
54
+ res = @evaluator.check_gate(user, gate_name)
55
+ if res.nil?
56
+ res = Statsig::ConfigResult.new(gate_name)
57
+ end
45
58
 
46
- if res == $fetch_from_server
47
- res = check_gate_fallback(user, gate_name)
48
- # exposure logged by the server
49
- else
50
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
51
- end
59
+ if res == $fetch_from_server
60
+ res = check_gate_fallback(user, gate_name)
61
+ # exposure logged by the server
62
+ else
63
+ @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
64
+ end
65
+
66
+ res.gate_value
67
+ }, -> { false })
52
68
 
53
- res.gate_value
54
69
  end
55
70
 
56
71
  sig { params(user: StatsigUser, dynamic_config_name: String).returns(DynamicConfig) }
57
72
 
58
73
  def get_config(user, dynamic_config_name)
59
- user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
60
- get_config_impl(user, dynamic_config_name)
74
+ @err_boundary.capture(-> {
75
+ user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
76
+ get_config_impl(user, dynamic_config_name)
77
+ }, -> { DynamicConfig.new(dynamic_config_name) })
61
78
  end
62
79
 
63
80
  sig { params(user: StatsigUser, experiment_name: String).returns(DynamicConfig) }
64
81
 
65
82
  def get_experiment(user, experiment_name)
66
- user = verify_inputs(user, experiment_name, "experiment_name")
67
- get_config_impl(user, experiment_name)
83
+ @err_boundary.capture(-> {
84
+ user = verify_inputs(user, experiment_name, "experiment_name")
85
+ get_config_impl(user, experiment_name)
86
+ }, -> { DynamicConfig.new(experiment_name) })
68
87
  end
69
88
 
70
89
  sig { params(user: StatsigUser, layer_name: String).returns(Layer) }
71
90
 
72
91
  def get_layer(user, layer_name)
73
- user = verify_inputs(user, layer_name, "layer_name")
92
+ @err_boundary.capture(-> {
93
+ user = verify_inputs(user, layer_name, "layer_name")
74
94
 
75
- res = @evaluator.get_layer(user, layer_name)
76
- if res.nil?
77
- res = Statsig::ConfigResult.new(layer_name)
78
- end
95
+ res = @evaluator.get_layer(user, layer_name)
96
+ if res.nil?
97
+ res = Statsig::ConfigResult.new(layer_name)
98
+ end
79
99
 
80
- if res == $fetch_from_server
81
- if res.config_delegate.empty?
82
- return Layer.new(layer_name)
100
+ if res == $fetch_from_server
101
+ if res.config_delegate.empty?
102
+ return Layer.new(layer_name)
103
+ end
104
+ res = get_config_fallback(user, res.config_delegate)
105
+ # exposure logged by the server
83
106
  end
84
- res = get_config_fallback(user, res.config_delegate)
85
- # exposure logged by the server
86
- end
87
107
 
88
- Layer.new(res.name, res.json_value, res.rule_id, lambda { |layer, parameter_name|
89
- @logger.log_layer_exposure(user, layer, parameter_name, res)
108
+ Layer.new(res.name, res.json_value, res.rule_id, lambda { |layer, parameter_name|
109
+ @logger.log_layer_exposure(user, layer, parameter_name, res)
110
+ })
111
+ }, -> {
112
+ Layer.new(layer_name)
90
113
  })
91
114
  end
92
115
 
93
116
  def log_event(user, event_name, value = nil, metadata = nil)
94
- if !user.nil? && !user.instance_of?(StatsigUser)
95
- raise 'Must provide a valid StatsigUser or nil'
96
- end
97
- check_shutdown
117
+ @err_boundary.capture(-> {
118
+ if !user.nil? && !user.instance_of?(StatsigUser)
119
+ raise Statsig::ValueError.new('Must provide a valid StatsigUser or nil')
120
+ end
121
+ check_shutdown
98
122
 
99
- user = normalize_user(user)
123
+ user = normalize_user(user)
100
124
 
101
- event = StatsigEvent.new(event_name)
102
- event.user = user
103
- event.value = value
104
- event.metadata = metadata
105
- event.statsig_metadata = Statsig.get_statsig_metadata
106
- @logger.log_event(event)
125
+ event = StatsigEvent.new(event_name)
126
+ event.user = user
127
+ event.value = value
128
+ event.metadata = metadata
129
+ event.statsig_metadata = Statsig.get_statsig_metadata
130
+ @logger.log_event(event)
131
+ })
107
132
  end
108
133
 
109
134
  def shutdown
110
- @shutdown = true
111
- @logger.shutdown
112
- @evaluator.shutdown
135
+ @err_boundary.capture(-> {
136
+ @shutdown = true
137
+ @logger.shutdown
138
+ @evaluator.shutdown
139
+ })
113
140
  end
114
141
 
115
142
  def override_gate(gate_name, gate_value)
116
- @evaluator.override_gate(gate_name, gate_value)
143
+ @err_boundary.capture(-> {
144
+ @evaluator.override_gate(gate_name, gate_value)
145
+ })
117
146
  end
118
147
 
119
148
  def override_config(config_name, config_value)
120
- @evaluator.override_config(config_name, config_value)
149
+ @err_boundary.capture(-> {
150
+ @evaluator.override_config(config_name, config_value)
151
+ })
121
152
  end
122
153
 
123
154
  # @param [StatsigUser] user
124
155
  # @return [Hash]
125
156
  def get_client_initialize_response(user)
126
- normalize_user(user)
127
- @evaluator.get_client_initialize_response(user)
157
+ @err_boundary.capture(-> {
158
+ normalize_user(user)
159
+ @evaluator.get_client_initialize_response(user)
160
+ }, -> { nil })
128
161
  end
129
162
 
130
163
  def maybe_restart_background_threads
131
- @evaluator.maybe_restart_background_threads
132
- @logger.maybe_restart_background_threads
164
+ if @options.local_mode
165
+ return
166
+ end
167
+
168
+ @err_boundary.capture(-> {
169
+ @evaluator.maybe_restart_background_threads
170
+ @logger.maybe_restart_background_threads
171
+ })
133
172
  end
134
173
 
135
174
  private
136
175
 
176
+ sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
177
+
137
178
  def verify_inputs(user, config_name, variable_name)
138
179
  validate_user(user)
139
180
  if !config_name.is_a?(String) || config_name.empty?
140
- raise "Invalid #{variable_name} provided"
181
+ raise Statsig::ValueError.new("Invalid #{variable_name} provided")
141
182
  end
142
183
 
143
184
  check_shutdown
185
+ maybe_restart_background_threads
144
186
  normalize_user(user)
145
187
  end
146
188
 
@@ -168,7 +210,7 @@ class StatsigDriver
168
210
  !user.user_id.is_a?(String) &&
169
211
  (!user.custom_ids.is_a?(Hash) || user.custom_ids.size == 0)
170
212
  )
171
- raise 'Must provide a valid StatsigUser with a user_id or at least a custom ID. See https://docs.statsig.com/messages/serverRequiredUserID/ for more details.'
213
+ raise Statsig::ValueError.new('Must provide a valid StatsigUser with a user_id or at least a custom ID. See https://docs.statsig.com/messages/serverRequiredUserID/ for more details.')
172
214
  end
173
215
  end
174
216
 
@@ -214,4 +256,12 @@ class StatsigDriver
214
256
  network_result['rule_id'],
215
257
  )
216
258
  end
217
- end
259
+
260
+ def log_init_diagnostics
261
+ if @options.disable_diagnostics_logging
262
+ return
263
+ end
264
+
265
+ @logger.log_diagnostics_event(@init_diagnostics)
266
+ end
267
+ end
@@ -0,0 +1,11 @@
1
+ module Statsig
2
+ class UninitializedError < StandardError
3
+ def initialize(msg="Must call initialize first.")
4
+ super
5
+ end
6
+ end
7
+
8
+ class ValueError < StandardError
9
+
10
+ end
11
+ end
@@ -5,6 +5,7 @@ require 'concurrent-ruby'
5
5
  $gate_exposure_event = 'statsig::gate_exposure'
6
6
  $config_exposure_event = 'statsig::config_exposure'
7
7
  $layer_exposure_event = 'statsig::layer_exposure'
8
+ $diagnostics_event = 'statsig::diagnostics'
8
9
 
9
10
  module Statsig
10
11
  class StatsigLogger
@@ -85,6 +86,13 @@ module Statsig
85
86
  log_event(event)
86
87
  end
87
88
 
89
+ def log_diagnostics_event(diagnostics, user = nil)
90
+ event = StatsigEvent.new($diagnostics_event)
91
+ event.user = user
92
+ event.metadata = diagnostics.serialize
93
+ log_event(event)
94
+ end
95
+
88
96
  def periodic_flush
89
97
  Thread.new do
90
98
  loop do
@@ -64,6 +64,11 @@ class StatsigOptions
64
64
  # default: 3
65
65
  attr_accessor :idlist_threadpool_size
66
66
 
67
+ sig { returns(T::Boolean) }
68
+ # Should diagnostics be logged. These include performance metrics for initialize.
69
+ # default: false
70
+ attr_accessor :disable_diagnostics_logging
71
+
67
72
  sig do
68
73
  params(
69
74
  environment: T.any(T::Hash[String, String], NilClass),
@@ -76,7 +81,8 @@ class StatsigOptions
76
81
  bootstrap_values: T.any(String, NilClass),
77
82
  rules_updated_callback: T.any(Method, Proc, NilClass),
78
83
  data_store: T.any(Statsig::Interfaces::IDataStore, NilClass),
79
- idlist_threadpool_size: Integer
84
+ idlist_threadpool_size: Integer,
85
+ disable_diagnostics_logging: T::Boolean
80
86
  ).void
81
87
  end
82
88
 
@@ -91,7 +97,8 @@ class StatsigOptions
91
97
  bootstrap_values: nil,
92
98
  rules_updated_callback: nil,
93
99
  data_store: nil,
94
- idlist_threadpool_size: 3)
100
+ idlist_threadpool_size: 3,
101
+ disable_diagnostics_logging: false)
95
102
  @environment = environment.is_a?(Hash) ? environment : nil
96
103
  @api_url_base = api_url_base
97
104
  @rulesets_sync_interval = rulesets_sync_interval
@@ -103,5 +110,6 @@ class StatsigOptions
103
110
  @rules_updated_callback = rules_updated_callback
104
111
  @data_store = data_store
105
112
  @idlist_threadpool_size = idlist_threadpool_size
113
+ @disable_diagnostics_logging = disable_diagnostics_logging
106
114
  end
107
115
  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.17.0
4
+ version: 1.18.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: 2022-11-01 00:00:00.000000000 Z
11
+ date: 2022-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sorbet
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.5.10461
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.5.10461
83
+ - !ruby/object:Gem::Dependency
84
+ name: tapioca
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.4.27
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.4.27
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: user_agent_parser
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -118,16 +146,16 @@ dependencies:
118
146
  name: sorbet-runtime
119
147
  requirement: !ruby/object:Gem::Requirement
120
148
  requirements:
121
- - - ">="
149
+ - - '='
122
150
  - !ruby/object:Gem::Version
123
- version: '0'
151
+ version: 0.5.10461
124
152
  type: :runtime
125
153
  prerelease: false
126
154
  version_requirements: !ruby/object:Gem::Requirement
127
155
  requirements:
128
- - - ">="
156
+ - - '='
129
157
  - !ruby/object:Gem::Version
130
- version: '0'
158
+ version: 0.5.10461
131
159
  - !ruby/object:Gem::Dependency
132
160
  name: concurrent-ruby
133
161
  requirement: !ruby/object:Gem::Requirement
@@ -150,7 +178,9 @@ extra_rdoc_files: []
150
178
  files:
151
179
  - lib/client_initialize_helpers.rb
152
180
  - lib/config_result.rb
181
+ - lib/diagnostics.rb
153
182
  - lib/dynamic_config.rb
183
+ - lib/error_boundary.rb
154
184
  - lib/evaluation_details.rb
155
185
  - lib/evaluation_helpers.rb
156
186
  - lib/evaluator.rb
@@ -161,6 +191,7 @@ files:
161
191
  - lib/spec_store.rb
162
192
  - lib/statsig.rb
163
193
  - lib/statsig_driver.rb
194
+ - lib/statsig_errors.rb
164
195
  - lib/statsig_event.rb
165
196
  - lib/statsig_logger.rb
166
197
  - lib/statsig_options.rb
@@ -184,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
215
  - !ruby/object:Gem::Version
185
216
  version: '0'
186
217
  requirements: []
187
- rubygems_version: 3.3.22
218
+ rubygems_version: 3.3.11
188
219
  signing_key:
189
220
  specification_version: 4
190
221
  summary: Statsig server SDK for Ruby