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 +4 -4
- data/lib/diagnostics.rb +44 -0
- data/lib/error_boundary.rb +57 -0
- data/lib/evaluator.rb +2 -2
- data/lib/network.rb +24 -11
- data/lib/spec_store.rb +48 -16
- data/lib/statsig.rb +9 -9
- data/lib/statsig_driver.rb +110 -60
- data/lib/statsig_errors.rb +11 -0
- data/lib/statsig_logger.rb +8 -0
- data/lib/statsig_options.rb +10 -2
- metadata +38 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 475c2e56b1f53dbc642adff47dd6a1de3a3c324ce9a0145fcaf7c6ebb2b412c1
|
4
|
+
data.tar.gz: a37bbef25c2ce1b818fafe25252bcf076964b16acdb484d41ebe2c705b6042b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32ba1629776babb0f4d437e551a251e5bea38b42b916c067c0009705ef59b71db5989a6942e009994ab5956e10ca61cf7ee4a14f9302312094e2b2cc479568c5
|
7
|
+
data.tar.gz: d8d6f082a647a6b6ba7d6adf8954c17d845b44b55dfd7b17ce4fff0aa858cbce96d6308e2ec0e0f15209275d5e8520f0b5697d1caf64cdac7e40e0d7dc46b4da
|
data/lib/diagnostics.rb
ADDED
@@ -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
|
-
|
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
|
-
{
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
51
|
-
return nil,
|
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
|
-
|
46
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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.
|
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
|
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
|
data/lib/statsig_driver.rb
CHANGED
@@ -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
|
-
@
|
29
|
-
@
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
51
|
+
@err_boundary.capture(-> {
|
52
|
+
user = verify_inputs(user, gate_name, "gate_name")
|
40
53
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
54
|
+
res = @evaluator.check_gate(user, gate_name)
|
55
|
+
if res.nil?
|
56
|
+
res = Statsig::ConfigResult.new(gate_name)
|
57
|
+
end
|
45
58
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
92
|
+
@err_boundary.capture(-> {
|
93
|
+
user = verify_inputs(user, layer_name, "layer_name")
|
74
94
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
95
|
+
res = @evaluator.get_layer(user, layer_name)
|
96
|
+
if res.nil?
|
97
|
+
res = Statsig::ConfigResult.new(layer_name)
|
98
|
+
end
|
79
99
|
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
123
|
+
user = normalize_user(user)
|
100
124
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
@
|
111
|
-
|
112
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
127
|
-
|
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
|
-
@
|
132
|
-
|
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
|
-
|
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
|
data/lib/statsig_logger.rb
CHANGED
@@ -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
|
data/lib/statsig_options.rb
CHANGED
@@ -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.
|
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-
|
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:
|
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:
|
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.
|
218
|
+
rubygems_version: 3.3.11
|
188
219
|
signing_key:
|
189
220
|
specification_version: 4
|
190
221
|
summary: Statsig server SDK for Ruby
|