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 +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
|