statsig 1.17.0 → 1.18.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: 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