statsig 1.17.0 → 1.19.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 +98 -9
- data/lib/statsig_driver.rb +165 -69
- data/lib/statsig_errors.rb +11 -0
- data/lib/statsig_logger.rb +24 -3
- 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: 354a77a658a3232a8695d82136d3e84e5bf0ce2199d06af9fee216eaae7d5dcd
|
4
|
+
data.tar.gz: 3be67ca270e61eedd92fb3aaa96b4f28ff1b79feee3ec4b1a33bdc2d000f8c2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f2461d11144a2e822178d69dad1ff50ebb2a4cd14f7b5dc46704b9c285a56d494f512b8058361f0a3ba3c71d899a6b9afc2fe51618abbe73784c6a937c39125
|
7
|
+
data.tar.gz: 45ac2d3a06f60c1cb0a81b109df9d06e1f479d384023e2009343b6d6f74b5d806fdf86e85b5d09ad8f1e55b07ca8c1b010cb23f7c8de2e9594b1092b323c8708
|
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.
|
@@ -35,6 +35,28 @@ module Statsig
|
|
35
35
|
@shared_instance&.check_gate(user, gate_name)
|
36
36
|
end
|
37
37
|
|
38
|
+
sig { params(user: StatsigUser, gate_name: String).returns(T::Boolean) }
|
39
|
+
##
|
40
|
+
# Gets the boolean result of a gate, evaluated against the given user.
|
41
|
+
#
|
42
|
+
# @param user A StatsigUser object used for the evaluation
|
43
|
+
# @param gate_name The name of the gate being checked
|
44
|
+
def self.check_gate_with_exposure_logging_disabled(user, gate_name)
|
45
|
+
ensure_initialized
|
46
|
+
@shared_instance&.check_gate(user, gate_name, StatsigDriver::CheckGateOptions.new(log_exposure: false))
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(user: StatsigUser, gate_name: String).void }
|
50
|
+
##
|
51
|
+
# Logs an exposure event for the gate
|
52
|
+
#
|
53
|
+
# @param user A StatsigUser object used for the evaluation
|
54
|
+
# @param gate_name The name of the gate being checked
|
55
|
+
def self.manually_log_gate_exposure(user, gate_name)
|
56
|
+
ensure_initialized
|
57
|
+
@shared_instance&.manually_log_gate_exposure(user, gate_name)
|
58
|
+
end
|
59
|
+
|
38
60
|
sig { params(user: StatsigUser, dynamic_config_name: String).returns(DynamicConfig) }
|
39
61
|
##
|
40
62
|
# Get the values of a dynamic config, evaluated against the given user. An exposure event will automatically be logged for the dynamic config.
|
@@ -46,6 +68,28 @@ module Statsig
|
|
46
68
|
@shared_instance&.get_config(user, dynamic_config_name)
|
47
69
|
end
|
48
70
|
|
71
|
+
sig { params(user: StatsigUser, dynamic_config_name: String).returns(DynamicConfig) }
|
72
|
+
##
|
73
|
+
# Get the values of a dynamic config, evaluated against the given user.
|
74
|
+
#
|
75
|
+
# @param user A StatsigUser object used for the evaluation
|
76
|
+
# @param dynamic_config_name The name of the dynamic config
|
77
|
+
def self.get_config_with_exposure_logging_disabled(user, dynamic_config_name)
|
78
|
+
ensure_initialized
|
79
|
+
@shared_instance&.get_config(user, dynamic_config_name, StatsigDriver::GetConfigOptions.new(log_exposure: false))
|
80
|
+
end
|
81
|
+
|
82
|
+
sig { params(user: StatsigUser, dynamic_config: String).void }
|
83
|
+
##
|
84
|
+
# Logs an exposure event for the dynamic config
|
85
|
+
#
|
86
|
+
# @param user A StatsigUser object used for the evaluation
|
87
|
+
# @param dynamic_config_name The name of the dynamic config
|
88
|
+
def self.manually_log_config_exposure(user, dynamic_config)
|
89
|
+
ensure_initialized
|
90
|
+
@shared_instance&.manually_log_config_exposure(user, dynamic_config)
|
91
|
+
end
|
92
|
+
|
49
93
|
sig { params(user: StatsigUser, experiment_name: String).returns(DynamicConfig) }
|
50
94
|
##
|
51
95
|
# Get the values of an experiment, evaluated against the given user. An exposure event will automatically be logged for the experiment.
|
@@ -57,6 +101,28 @@ module Statsig
|
|
57
101
|
@shared_instance&.get_experiment(user, experiment_name)
|
58
102
|
end
|
59
103
|
|
104
|
+
sig { params(user: StatsigUser, experiment_name: String).returns(DynamicConfig) }
|
105
|
+
##
|
106
|
+
# Get the values of an experiment, evaluated against the given user.
|
107
|
+
#
|
108
|
+
# @param user A StatsigUser object used for the evaluation
|
109
|
+
# @param experiment_name The name of the experiment
|
110
|
+
def self.get_experiment_with_exposure_logging_disabled(user, experiment_name)
|
111
|
+
ensure_initialized
|
112
|
+
@shared_instance&.get_experiment(user, experiment_name, StatsigDriver::GetExperimentOptions.new(log_exposure: false))
|
113
|
+
end
|
114
|
+
|
115
|
+
sig { params(user: StatsigUser, experiment_name: String).void }
|
116
|
+
##
|
117
|
+
# Logs an exposure event for the experiment
|
118
|
+
#
|
119
|
+
# @param user A StatsigUser object used for the evaluation
|
120
|
+
# @param experiment_name The name of the experiment
|
121
|
+
def self.manually_log_experiment_exposure(user, experiment_name)
|
122
|
+
ensure_initialized
|
123
|
+
@shared_instance&.manually_log_config_exposure(user, experiment_name)
|
124
|
+
end
|
125
|
+
|
60
126
|
sig { params(user: StatsigUser, layer_name: String).returns(Layer) }
|
61
127
|
##
|
62
128
|
# Get the values of a layer, evaluated against the given user.
|
@@ -69,6 +135,29 @@ module Statsig
|
|
69
135
|
@shared_instance&.get_layer(user, layer_name)
|
70
136
|
end
|
71
137
|
|
138
|
+
sig { params(user: StatsigUser, layer_name: String).returns(Layer) }
|
139
|
+
##
|
140
|
+
# Get the values of a layer, evaluated against the given user.
|
141
|
+
#
|
142
|
+
# @param user A StatsigUser object used for the evaluation
|
143
|
+
# @param layer_name The name of the layer
|
144
|
+
def self.get_layer_with_exposure_logging_disabled(user, layer_name)
|
145
|
+
ensure_initialized
|
146
|
+
@shared_instance&.get_layer(user, layer_name, StatsigDriver::GetLayerOptions.new(log_exposure: false))
|
147
|
+
end
|
148
|
+
|
149
|
+
sig { params(user: StatsigUser, layer_name: String, parameter_name: String).returns(Layer) }
|
150
|
+
##
|
151
|
+
# Logs an exposure event for the parameter in the given layer
|
152
|
+
#
|
153
|
+
# @param user A StatsigUser object used for the evaluation
|
154
|
+
# @param layer_name The name of the layer
|
155
|
+
# @param parameter_name The name of the parameter in the layer
|
156
|
+
def self.manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
157
|
+
ensure_initialized
|
158
|
+
@shared_instance&.manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
159
|
+
end
|
160
|
+
|
72
161
|
sig { params(user: StatsigUser,
|
73
162
|
event_name: String,
|
74
163
|
value: T.any(String, Integer, Float, NilClass),
|
@@ -89,7 +178,7 @@ module Statsig
|
|
89
178
|
##
|
90
179
|
# Stops all Statsig activity and flushes any pending events.
|
91
180
|
def self.shutdown
|
92
|
-
|
181
|
+
if defined? @shared_instance and !@shared_instance.nil?
|
93
182
|
@shared_instance.shutdown
|
94
183
|
end
|
95
184
|
@shared_instance = nil
|
@@ -136,32 +225,32 @@ module Statsig
|
|
136
225
|
def self.get_statsig_metadata
|
137
226
|
{
|
138
227
|
'sdkType' => 'ruby-server',
|
139
|
-
'sdkVersion' => '1.
|
228
|
+
'sdkVersion' => '1.19.0',
|
140
229
|
}
|
141
230
|
end
|
142
231
|
|
143
232
|
private
|
144
233
|
|
145
234
|
def self.ensure_initialized
|
146
|
-
if @shared_instance.nil?
|
147
|
-
raise
|
235
|
+
if not defined? @shared_instance or @shared_instance.nil?
|
236
|
+
raise Statsig::UninitializedError.new
|
148
237
|
end
|
149
238
|
end
|
150
239
|
|
151
240
|
T::Configuration.call_validation_error_handler = lambda do |signature, opts|
|
152
|
-
puts opts[:pretty_message]
|
241
|
+
puts "[Type Error] " + opts[:pretty_message]
|
153
242
|
end
|
154
243
|
|
155
244
|
T::Configuration.inline_type_error_handler = lambda do |error, opts|
|
156
|
-
puts error.message
|
245
|
+
puts "[Type Error] " + error.message
|
157
246
|
end
|
158
247
|
|
159
248
|
T::Configuration.sig_builder_error_handler = lambda do |error, location|
|
160
|
-
puts error.message
|
249
|
+
puts "[Type Error] " + error.message
|
161
250
|
end
|
162
251
|
|
163
252
|
T::Configuration.sig_validation_error_handler = lambda do |error, opts|
|
164
|
-
puts error.message
|
253
|
+
puts "[Type Error] " + error.message
|
165
254
|
end
|
166
255
|
|
167
256
|
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,132 +22,215 @@ 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
|
+
})
|
46
|
+
end
|
47
|
+
|
48
|
+
class CheckGateOptions < T::Struct
|
49
|
+
prop :log_exposure, T::Boolean, default: true
|
34
50
|
end
|
35
51
|
|
36
|
-
sig { params(user: StatsigUser, gate_name: String).returns(T::Boolean) }
|
52
|
+
sig { params(user: StatsigUser, gate_name: String, options: CheckGateOptions).returns(T::Boolean) }
|
53
|
+
|
54
|
+
def check_gate(user, gate_name, options = CheckGateOptions.new)
|
55
|
+
@err_boundary.capture(-> {
|
56
|
+
user = verify_inputs(user, gate_name, "gate_name")
|
57
|
+
|
58
|
+
res = @evaluator.check_gate(user, gate_name)
|
59
|
+
if res.nil?
|
60
|
+
res = Statsig::ConfigResult.new(gate_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
if res == $fetch_from_server
|
64
|
+
res = check_gate_fallback(user, gate_name)
|
65
|
+
# exposure logged by the server
|
66
|
+
else
|
67
|
+
if options.log_exposure
|
68
|
+
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
res.gate_value
|
73
|
+
}, -> { false })
|
37
74
|
|
38
|
-
|
39
|
-
user = verify_inputs(user, gate_name, "gate_name")
|
75
|
+
end
|
40
76
|
|
77
|
+
sig { params(user: StatsigUser, gate_name: String).void }
|
78
|
+
|
79
|
+
def manually_log_gate_exposure(user, gate_name)
|
41
80
|
res = @evaluator.check_gate(user, gate_name)
|
42
|
-
|
43
|
-
|
44
|
-
|
81
|
+
context = {'is_manual_exposure' => true}
|
82
|
+
@logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
83
|
+
end
|
45
84
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
85
|
+
class GetConfigOptions < T::Struct
|
86
|
+
prop :log_exposure, T::Boolean, default: true
|
87
|
+
end
|
88
|
+
|
89
|
+
sig { params(user: StatsigUser, dynamic_config_name: String, options: GetConfigOptions).returns(DynamicConfig) }
|
52
90
|
|
53
|
-
|
91
|
+
def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
|
92
|
+
@err_boundary.capture(-> {
|
93
|
+
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
94
|
+
get_config_impl(user, dynamic_config_name, options)
|
95
|
+
}, -> { DynamicConfig.new(dynamic_config_name) })
|
54
96
|
end
|
55
97
|
|
56
|
-
|
98
|
+
class GetExperimentOptions < T::Struct
|
99
|
+
prop :log_exposure, T::Boolean, default: true
|
100
|
+
end
|
101
|
+
|
102
|
+
sig { params(user: StatsigUser, experiment_name: String, options: GetExperimentOptions).returns(DynamicConfig) }
|
57
103
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
104
|
+
def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
|
105
|
+
@err_boundary.capture(-> {
|
106
|
+
user = verify_inputs(user, experiment_name, "experiment_name")
|
107
|
+
get_config_impl(user, experiment_name, options)
|
108
|
+
}, -> { DynamicConfig.new(experiment_name) })
|
61
109
|
end
|
62
110
|
|
63
|
-
sig { params(user: StatsigUser,
|
111
|
+
sig { params(user: StatsigUser, config_name: String).void }
|
64
112
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
113
|
+
def manually_log_config_exposure(user, config_name)
|
114
|
+
res = @evaluator.get_config(user, config_name)
|
115
|
+
context = {'is_manual_exposure' => true}
|
116
|
+
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
68
117
|
end
|
69
118
|
|
70
|
-
|
119
|
+
class GetLayerOptions < T::Struct
|
120
|
+
prop :log_exposure, T::Boolean, default: true
|
121
|
+
end
|
71
122
|
|
72
|
-
|
73
|
-
user = verify_inputs(user, layer_name, "layer_name")
|
123
|
+
sig { params(user: StatsigUser, layer_name: String, options: GetLayerOptions).returns(Layer) }
|
74
124
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
125
|
+
def get_layer(user, layer_name, options = GetLayerOptions.new)
|
126
|
+
@err_boundary.capture(-> {
|
127
|
+
user = verify_inputs(user, layer_name, "layer_name")
|
79
128
|
|
80
|
-
|
81
|
-
if res.
|
82
|
-
|
129
|
+
res = @evaluator.get_layer(user, layer_name)
|
130
|
+
if res.nil?
|
131
|
+
res = Statsig::ConfigResult.new(layer_name)
|
83
132
|
end
|
84
|
-
res = get_config_fallback(user, res.config_delegate)
|
85
|
-
# exposure logged by the server
|
86
|
-
end
|
87
133
|
|
88
|
-
|
89
|
-
|
134
|
+
if res == $fetch_from_server
|
135
|
+
if res.config_delegate.empty?
|
136
|
+
return Layer.new(layer_name)
|
137
|
+
end
|
138
|
+
res = get_config_fallback(user, res.config_delegate)
|
139
|
+
# exposure logged by the server
|
140
|
+
end
|
141
|
+
|
142
|
+
exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
|
143
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
144
|
+
} : nil
|
145
|
+
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
146
|
+
}, -> {
|
147
|
+
Layer.new(layer_name)
|
90
148
|
})
|
91
149
|
end
|
92
150
|
|
151
|
+
sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
|
152
|
+
|
153
|
+
def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
154
|
+
res = @evaluator.get_layer(user, layer_name)
|
155
|
+
layer = Layer.new(layer_name, res.json_value, res.rule_id)
|
156
|
+
context = {'is_manual_exposure' => true}
|
157
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res, context)
|
158
|
+
end
|
159
|
+
|
93
160
|
def log_event(user, event_name, value = nil, metadata = nil)
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
161
|
+
@err_boundary.capture(-> {
|
162
|
+
if !user.nil? && !user.instance_of?(StatsigUser)
|
163
|
+
raise Statsig::ValueError.new('Must provide a valid StatsigUser or nil')
|
164
|
+
end
|
165
|
+
check_shutdown
|
98
166
|
|
99
|
-
|
167
|
+
user = normalize_user(user)
|
100
168
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
169
|
+
event = StatsigEvent.new(event_name)
|
170
|
+
event.user = user
|
171
|
+
event.value = value
|
172
|
+
event.metadata = metadata
|
173
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
174
|
+
@logger.log_event(event)
|
175
|
+
})
|
107
176
|
end
|
108
177
|
|
109
178
|
def shutdown
|
110
|
-
@
|
111
|
-
|
112
|
-
|
179
|
+
@err_boundary.capture(-> {
|
180
|
+
@shutdown = true
|
181
|
+
@logger.shutdown
|
182
|
+
@evaluator.shutdown
|
183
|
+
})
|
113
184
|
end
|
114
185
|
|
115
186
|
def override_gate(gate_name, gate_value)
|
116
|
-
@
|
187
|
+
@err_boundary.capture(-> {
|
188
|
+
@evaluator.override_gate(gate_name, gate_value)
|
189
|
+
})
|
117
190
|
end
|
118
191
|
|
119
192
|
def override_config(config_name, config_value)
|
120
|
-
@
|
193
|
+
@err_boundary.capture(-> {
|
194
|
+
@evaluator.override_config(config_name, config_value)
|
195
|
+
})
|
121
196
|
end
|
122
197
|
|
123
198
|
# @param [StatsigUser] user
|
124
199
|
# @return [Hash]
|
125
200
|
def get_client_initialize_response(user)
|
126
|
-
|
127
|
-
|
201
|
+
@err_boundary.capture(-> {
|
202
|
+
normalize_user(user)
|
203
|
+
@evaluator.get_client_initialize_response(user)
|
204
|
+
}, -> { nil })
|
128
205
|
end
|
129
206
|
|
130
207
|
def maybe_restart_background_threads
|
131
|
-
@
|
132
|
-
|
208
|
+
if @options.local_mode
|
209
|
+
return
|
210
|
+
end
|
211
|
+
|
212
|
+
@err_boundary.capture(-> {
|
213
|
+
@evaluator.maybe_restart_background_threads
|
214
|
+
@logger.maybe_restart_background_threads
|
215
|
+
})
|
133
216
|
end
|
134
217
|
|
135
218
|
private
|
136
219
|
|
220
|
+
sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
|
221
|
+
|
137
222
|
def verify_inputs(user, config_name, variable_name)
|
138
223
|
validate_user(user)
|
139
224
|
if !config_name.is_a?(String) || config_name.empty?
|
140
|
-
raise "Invalid #{variable_name} provided"
|
225
|
+
raise Statsig::ValueError.new("Invalid #{variable_name} provided")
|
141
226
|
end
|
142
227
|
|
143
228
|
check_shutdown
|
229
|
+
maybe_restart_background_threads
|
144
230
|
normalize_user(user)
|
145
231
|
end
|
146
232
|
|
147
|
-
def get_config_impl(user, config_name)
|
233
|
+
def get_config_impl(user, config_name, options)
|
148
234
|
res = @evaluator.get_config(user, config_name)
|
149
235
|
if res.nil?
|
150
236
|
res = Statsig::ConfigResult.new(config_name)
|
@@ -154,7 +240,9 @@ class StatsigDriver
|
|
154
240
|
res = get_config_fallback(user, config_name)
|
155
241
|
# exposure logged by the server
|
156
242
|
else
|
157
|
-
|
243
|
+
if options.log_exposure
|
244
|
+
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
245
|
+
end
|
158
246
|
end
|
159
247
|
|
160
248
|
DynamicConfig.new(res.name, res.json_value, res.rule_id)
|
@@ -168,7 +256,7 @@ class StatsigDriver
|
|
168
256
|
!user.user_id.is_a?(String) &&
|
169
257
|
(!user.custom_ids.is_a?(Hash) || user.custom_ids.size == 0)
|
170
258
|
)
|
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.'
|
259
|
+
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
260
|
end
|
173
261
|
end
|
174
262
|
|
@@ -214,4 +302,12 @@ class StatsigDriver
|
|
214
302
|
network_result['rule_id'],
|
215
303
|
)
|
216
304
|
end
|
217
|
-
|
305
|
+
|
306
|
+
def log_init_diagnostics
|
307
|
+
if @options.disable_diagnostics_logging
|
308
|
+
return
|
309
|
+
end
|
310
|
+
|
311
|
+
@logger.log_diagnostics_event(@init_diagnostics)
|
312
|
+
end
|
313
|
+
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
|
@@ -31,7 +32,7 @@ module Statsig
|
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
|
-
def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures, eval_details)
|
35
|
+
def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures, eval_details, context = nil)
|
35
36
|
event = StatsigEvent.new($gate_exposure_event)
|
36
37
|
event.user = user
|
37
38
|
event.metadata = {
|
@@ -43,10 +44,11 @@ module Statsig
|
|
43
44
|
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
44
45
|
|
45
46
|
safe_add_eval_details(eval_details, event)
|
47
|
+
safe_add_exposure_context(context, event)
|
46
48
|
log_event(event)
|
47
49
|
end
|
48
50
|
|
49
|
-
def log_config_exposure(user, config_name, rule_id, secondary_exposures, eval_details)
|
51
|
+
def log_config_exposure(user, config_name, rule_id, secondary_exposures, eval_details, context = nil)
|
50
52
|
event = StatsigEvent.new($config_exposure_event)
|
51
53
|
event.user = user
|
52
54
|
event.metadata = {
|
@@ -57,10 +59,11 @@ module Statsig
|
|
57
59
|
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
58
60
|
|
59
61
|
safe_add_eval_details(eval_details, event)
|
62
|
+
safe_add_exposure_context(context, event)
|
60
63
|
log_event(event)
|
61
64
|
end
|
62
65
|
|
63
|
-
def log_layer_exposure(user, layer, parameter_name, config_evaluation)
|
66
|
+
def log_layer_exposure(user, layer, parameter_name, config_evaluation, context = nil)
|
64
67
|
exposures = config_evaluation.undelegated_sec_exps
|
65
68
|
allocated_experiment = ''
|
66
69
|
is_explicit = (config_evaluation.explicit_parameters&.include? parameter_name) || false
|
@@ -82,6 +85,14 @@ module Statsig
|
|
82
85
|
event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
|
83
86
|
|
84
87
|
safe_add_eval_details(config_evaluation.evaluation_details, event)
|
88
|
+
safe_add_exposure_context(context, event)
|
89
|
+
log_event(event)
|
90
|
+
end
|
91
|
+
|
92
|
+
def log_diagnostics_event(diagnostics, user = nil)
|
93
|
+
event = StatsigEvent.new($diagnostics_event)
|
94
|
+
event.user = user
|
95
|
+
event.metadata = diagnostics.serialize
|
85
96
|
log_event(event)
|
86
97
|
end
|
87
98
|
|
@@ -136,5 +147,15 @@ module Statsig
|
|
136
147
|
event.metadata['initTime'] = eval_details.init_time
|
137
148
|
event.metadata['serverTime'] = eval_details.server_time
|
138
149
|
end
|
150
|
+
|
151
|
+
def safe_add_exposure_context(context, event)
|
152
|
+
if context.nil?
|
153
|
+
return
|
154
|
+
end
|
155
|
+
|
156
|
+
if context['is_manual_exposure']
|
157
|
+
event.metadata['isManualExposure'] = 'true'
|
158
|
+
end
|
159
|
+
end
|
139
160
|
end
|
140
161
|
end
|
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.19.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
|
+
date: 2022-12-01 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.7
|
188
219
|
signing_key:
|
189
220
|
specification_version: 4
|
190
221
|
summary: Statsig server SDK for Ruby
|