statsig 1.17.0 → 1.19.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 +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
|