statsig 1.29.0.pre.beta.1 → 1.30.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/client_initialize_helpers.rb +1 -1
- data/lib/config_result.rb +11 -4
- data/lib/error_boundary.rb +2 -0
- data/lib/evaluator.rb +28 -10
- data/lib/feature_gate.rb +70 -0
- data/lib/layer.rb +17 -3
- data/lib/network.rb +45 -7
- data/lib/spec_store.rb +22 -3
- data/lib/statsig.rb +64 -1
- data/lib/statsig_driver.rb +85 -21
- data/lib/statsig_errors.rb +6 -0
- data/lib/statsig_options.rb +5 -6
- data/lib/uri_helper.rb +1 -1
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72119a11268473774b98457f89017155813cdaab4c19b8f07e1aad370ae6dd17
|
4
|
+
data.tar.gz: 69cb96c04c6b9c322bb239332b8c406a6b543bb42b0cab6da9e641d44fa15371
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d75c0a0d9c6529843c554d05b3ae49e6d863cd01da11f932e679bddddffab4287ef036af37c3cd8fff9310c10cd6758bc6e5435f78626245c5d1f315b91e09eb
|
7
|
+
data.tar.gz: b9886d0b78d1491393cfacdea4c1f4cdeae1d2a993a0f5a0875011be1083bb63e4a7721fb3a007869eaa7eaf69ad300438d8038c099e88277958d4daf6d226f7
|
@@ -42,7 +42,7 @@ module ClientInitializeHelpers
|
|
42
42
|
target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
|
43
43
|
config_target_apps = config_spec['targetAppIDs']
|
44
44
|
|
45
|
-
unless target_app_id.nil? || config_target_apps.nil?
|
45
|
+
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
46
46
|
return nil
|
47
47
|
end
|
48
48
|
|
data/lib/config_result.rb
CHANGED
@@ -18,6 +18,7 @@ module Statsig
|
|
18
18
|
attr_accessor :evaluation_details
|
19
19
|
attr_accessor :group_name
|
20
20
|
attr_accessor :id_type
|
21
|
+
attr_accessor :target_app_ids
|
21
22
|
|
22
23
|
def initialize(
|
23
24
|
name,
|
@@ -25,12 +26,13 @@ module Statsig
|
|
25
26
|
json_value = {},
|
26
27
|
rule_id = '',
|
27
28
|
secondary_exposures = [],
|
28
|
-
config_delegate =
|
29
|
+
config_delegate = nil,
|
29
30
|
explicit_parameters = [],
|
30
31
|
is_experiment_group: false,
|
31
32
|
evaluation_details: nil,
|
32
33
|
group_name: nil,
|
33
|
-
id_type: ''
|
34
|
+
id_type: '',
|
35
|
+
target_app_ids: nil)
|
34
36
|
@name = name
|
35
37
|
@gate_value = gate_value
|
36
38
|
@json_value = json_value
|
@@ -43,6 +45,7 @@ module Statsig
|
|
43
45
|
@evaluation_details = evaluation_details
|
44
46
|
@group_name = group_name
|
45
47
|
@id_type = id_type
|
48
|
+
@target_app_ids = target_app_ids
|
46
49
|
end
|
47
50
|
|
48
51
|
sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
|
@@ -62,7 +65,9 @@ module Statsig
|
|
62
65
|
hash['rule_id'],
|
63
66
|
hash['secondary_exposures'],
|
64
67
|
evaluation_details: EvaluationDetails.persisted(hash['config_sync_time'], hash['init_time']),
|
65
|
-
group_name: hash['group_name']
|
68
|
+
group_name: hash['group_name'],
|
69
|
+
id_type: hash['id_type'],
|
70
|
+
target_app_ids: hash['target_app_ids']
|
66
71
|
)
|
67
72
|
end
|
68
73
|
|
@@ -75,7 +80,9 @@ module Statsig
|
|
75
80
|
secondary_exposures: @secondary_exposures,
|
76
81
|
config_sync_time: @evaluation_details.config_sync_time,
|
77
82
|
init_time: @init_time,
|
78
|
-
group_name: @group_name
|
83
|
+
group_name: @group_name,
|
84
|
+
id_type: @id_type,
|
85
|
+
target_app_ids: @target_app_ids
|
79
86
|
}
|
80
87
|
end
|
81
88
|
end
|
data/lib/error_boundary.rb
CHANGED
@@ -24,6 +24,7 @@ module Statsig
|
|
24
24
|
end
|
25
25
|
|
26
26
|
puts '[Statsig]: An unexpected exception occurred.'
|
27
|
+
puts e.message
|
27
28
|
log_exception(e, tag: caller)
|
28
29
|
res = recover.call
|
29
30
|
end
|
@@ -45,6 +46,7 @@ module Statsig
|
|
45
46
|
'STATSIG-API-KEY' => @sdk_key,
|
46
47
|
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
47
48
|
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
49
|
+
'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
|
48
50
|
'Content-Type' => 'application/json; charset=UTF-8'
|
49
51
|
}).accept(:json)
|
50
52
|
body = {
|
data/lib/evaluator.rb
CHANGED
@@ -31,20 +31,16 @@ module Statsig
|
|
31
31
|
|
32
32
|
sig do
|
33
33
|
params(
|
34
|
-
|
34
|
+
store: SpecStore,
|
35
35
|
options: StatsigOptions,
|
36
|
-
error_callback: T.any(Method, Proc, NilClass),
|
37
|
-
diagnostics: Diagnostics,
|
38
|
-
error_boundary: ErrorBoundary,
|
39
|
-
logger: StatsigLogger,
|
40
36
|
persistent_storage_utils: UserPersistentStorageUtils,
|
41
37
|
).void
|
42
38
|
end
|
43
|
-
def initialize(
|
44
|
-
@spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics, error_boundary, logger)
|
39
|
+
def initialize(store, options, persistent_storage_utils)
|
45
40
|
UAParser.initialize_async
|
46
41
|
CountryLookup.initialize_async
|
47
42
|
|
43
|
+
@spec_store = store
|
48
44
|
@gate_overrides = {}
|
49
45
|
@config_overrides = {}
|
50
46
|
@options = options
|
@@ -122,7 +118,7 @@ module Statsig
|
|
122
118
|
@persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name, evaluation)
|
123
119
|
@persistent_storage_utils.save_to_storage(user, config['idType'], user_persisted_values)
|
124
120
|
end
|
125
|
-
|
121
|
+
# Otherwise, remove from persisted storage
|
126
122
|
else
|
127
123
|
@persistent_storage_utils.remove_experiment_from_storage(user, config['idType'], config_name)
|
128
124
|
evaluation = eval_spec(user, config)
|
@@ -143,6 +139,26 @@ module Statsig
|
|
143
139
|
eval_spec(user, @spec_store.get_layer(layer_name))
|
144
140
|
end
|
145
141
|
|
142
|
+
def list_gates
|
143
|
+
@spec_store.gates.map { |name, _| name }
|
144
|
+
end
|
145
|
+
|
146
|
+
def list_configs
|
147
|
+
@spec_store.configs.map { |name, config| name if config['entity'] == 'dynamic_config' }.compact
|
148
|
+
end
|
149
|
+
|
150
|
+
def list_experiments
|
151
|
+
@spec_store.configs.map { |name, config| name if config['entity'] == 'experiment' }.compact
|
152
|
+
end
|
153
|
+
|
154
|
+
def list_autotunes
|
155
|
+
@spec_store.configs.map { |name, config| name if config['entity'] == 'autotune' }.compact
|
156
|
+
end
|
157
|
+
|
158
|
+
def list_layers
|
159
|
+
@spec_store.layers.map { |name, _| name }
|
160
|
+
end
|
161
|
+
|
146
162
|
def get_client_initialize_response(user, hash, client_sdk_key)
|
147
163
|
if @spec_store.is_ready_for_checks == false
|
148
164
|
return nil
|
@@ -226,7 +242,8 @@ module Statsig
|
|
226
242
|
),
|
227
243
|
is_experiment_group: result.is_experiment_group,
|
228
244
|
group_name: result.group_name,
|
229
|
-
id_type: config['idType']
|
245
|
+
id_type: config['idType'],
|
246
|
+
target_app_ids: config['targetAppIDs']
|
230
247
|
)
|
231
248
|
end
|
232
249
|
|
@@ -248,7 +265,8 @@ module Statsig
|
|
248
265
|
@spec_store.init_reason
|
249
266
|
),
|
250
267
|
group_name: nil,
|
251
|
-
id_type: config['idType']
|
268
|
+
id_type: config['idType'],
|
269
|
+
target_app_ids: config['targetAppIDs']
|
252
270
|
)
|
253
271
|
end
|
254
272
|
|
data/lib/feature_gate.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
class FeatureGate
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { returns(String) }
|
9
|
+
attr_accessor :name
|
10
|
+
|
11
|
+
sig { returns(T::Boolean) }
|
12
|
+
attr_accessor :value
|
13
|
+
|
14
|
+
sig { returns(String) }
|
15
|
+
attr_accessor :rule_id
|
16
|
+
|
17
|
+
sig { returns(T.nilable(String)) }
|
18
|
+
attr_accessor :group_name
|
19
|
+
|
20
|
+
sig { returns(String) }
|
21
|
+
attr_accessor :id_type
|
22
|
+
|
23
|
+
sig { returns(T.nilable(Statsig::EvaluationDetails)) }
|
24
|
+
attr_accessor :evaluation_details
|
25
|
+
|
26
|
+
sig { returns(T.nilable(T::Array[String])) }
|
27
|
+
attr_accessor :target_app_ids
|
28
|
+
|
29
|
+
sig do
|
30
|
+
params(
|
31
|
+
name: String,
|
32
|
+
value: T::Boolean,
|
33
|
+
rule_id: String,
|
34
|
+
group_name: T.nilable(String),
|
35
|
+
id_type: String,
|
36
|
+
evaluation_details: T.nilable(Statsig::EvaluationDetails),
|
37
|
+
target_app_ids: T.nilable(T::Array[String])
|
38
|
+
).void
|
39
|
+
end
|
40
|
+
def initialize(
|
41
|
+
name,
|
42
|
+
value: false,
|
43
|
+
rule_id: '',
|
44
|
+
group_name: nil,
|
45
|
+
id_type: '',
|
46
|
+
evaluation_details: nil,
|
47
|
+
target_app_ids: nil
|
48
|
+
)
|
49
|
+
@name = name
|
50
|
+
@value = value
|
51
|
+
@rule_id = rule_id
|
52
|
+
@group_name = group_name
|
53
|
+
@id_type = id_type
|
54
|
+
@evaluation_details = evaluation_details
|
55
|
+
@target_app_ids = target_app_ids
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { params(res: Statsig::ConfigResult).returns(FeatureGate) }
|
59
|
+
def self.from_config_result(res)
|
60
|
+
new(
|
61
|
+
res.name,
|
62
|
+
value: res.gate_value,
|
63
|
+
rule_id: res.rule_id,
|
64
|
+
group_name: res.group_name,
|
65
|
+
id_type: res.id_type,
|
66
|
+
evaluation_details: res.evaluation_details,
|
67
|
+
target_app_ids: res.target_app_ids
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
data/lib/layer.rb
CHANGED
@@ -17,11 +17,25 @@ class Layer
|
|
17
17
|
sig { returns(String) }
|
18
18
|
attr_accessor :rule_id
|
19
19
|
|
20
|
-
sig {
|
21
|
-
|
20
|
+
sig { returns(String) }
|
21
|
+
attr_accessor :group_name
|
22
|
+
|
23
|
+
sig do
|
24
|
+
params(
|
25
|
+
name: String,
|
26
|
+
value: T::Hash[String, T.untyped],
|
27
|
+
rule_id: String,
|
28
|
+
group_name: T.nilable(String),
|
29
|
+
allocated_experiment: T.nilable(String),
|
30
|
+
exposure_log_func: T.any(Method, Proc, NilClass)
|
31
|
+
).void
|
32
|
+
end
|
33
|
+
def initialize(name, value = {}, rule_id = '', group_name = nil, allocated_experiment = nil, exposure_log_func = nil)
|
22
34
|
@name = name
|
23
35
|
@value = value
|
24
36
|
@rule_id = rule_id
|
37
|
+
@group_name = group_name
|
38
|
+
@allocated_experiment = allocated_experiment
|
25
39
|
@exposure_log_func = exposure_log_func
|
26
40
|
end
|
27
41
|
|
@@ -58,4 +72,4 @@ class Layer
|
|
58
72
|
|
59
73
|
@value[index]
|
60
74
|
end
|
61
|
-
end
|
75
|
+
end
|
data/lib/network.rb
CHANGED
@@ -42,6 +42,7 @@ module Statsig
|
|
42
42
|
'Content-Type' => 'application/json; charset=UTF-8',
|
43
43
|
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
44
44
|
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
45
|
+
'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
|
45
46
|
'Accept-Encoding' => 'gzip'
|
46
47
|
}
|
47
48
|
).accept(:json)
|
@@ -53,11 +54,42 @@ module Statsig
|
|
53
54
|
end
|
54
55
|
end
|
55
56
|
|
57
|
+
sig do
|
58
|
+
params(since_time: Integer)
|
59
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
60
|
+
end
|
61
|
+
def download_config_specs(since_time)
|
62
|
+
get("download_config_specs/#{@server_secret}.json?sinceTime=#{since_time}")
|
63
|
+
end
|
64
|
+
|
65
|
+
class HttpMethod < T::Enum
|
66
|
+
enums do
|
67
|
+
GET = new
|
68
|
+
POST = new
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
sig do
|
73
|
+
params(endpoint: String, retries: Integer, backoff: Integer)
|
74
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
75
|
+
end
|
76
|
+
def get(endpoint, retries = 0, backoff = 1)
|
77
|
+
request(HttpMethod::GET, endpoint, nil, retries, backoff)
|
78
|
+
end
|
79
|
+
|
56
80
|
sig do
|
57
81
|
params(endpoint: String, body: String, retries: Integer, backoff: Integer)
|
58
82
|
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
59
83
|
end
|
60
|
-
def
|
84
|
+
def post(endpoint, body, retries = 0, backoff = 1)
|
85
|
+
request(HttpMethod::POST, endpoint, body, retries, backoff)
|
86
|
+
end
|
87
|
+
|
88
|
+
sig do
|
89
|
+
params(method: HttpMethod, endpoint: String, body: T.nilable(String), retries: Integer, backoff: Integer)
|
90
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
91
|
+
end
|
92
|
+
def request(method, endpoint, body, retries = 0, backoff = 1)
|
61
93
|
if @local_mode
|
62
94
|
return nil, nil
|
63
95
|
end
|
@@ -73,14 +105,20 @@ module Statsig
|
|
73
105
|
url = URIHelper.build_url(endpoint)
|
74
106
|
begin
|
75
107
|
res = @connection_pool.with do |conn|
|
76
|
-
conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
|
108
|
+
request = conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
|
109
|
+
case method
|
110
|
+
when HttpMethod::GET
|
111
|
+
request.get(url)
|
112
|
+
when HttpMethod::POST
|
113
|
+
request.post(url, body: body)
|
114
|
+
end
|
77
115
|
end
|
78
116
|
rescue StandardError => e
|
79
117
|
## network error retry
|
80
118
|
return nil, e unless retries.positive?
|
81
119
|
|
82
120
|
sleep backoff_adjusted
|
83
|
-
return
|
121
|
+
return request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
84
122
|
end
|
85
123
|
return res, nil if res.status.success?
|
86
124
|
|
@@ -91,12 +129,12 @@ module Statsig
|
|
91
129
|
|
92
130
|
## status code retry
|
93
131
|
sleep backoff_adjusted
|
94
|
-
|
132
|
+
request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
95
133
|
end
|
96
134
|
|
97
135
|
def check_gate(user, gate_name)
|
98
136
|
request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
|
99
|
-
response, =
|
137
|
+
response, = post('check_gate', request_body)
|
100
138
|
return JSON.parse(response.body) unless response.nil?
|
101
139
|
|
102
140
|
false
|
@@ -106,7 +144,7 @@ module Statsig
|
|
106
144
|
|
107
145
|
def get_config(user, dynamic_config_name)
|
108
146
|
request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
|
109
|
-
response, =
|
147
|
+
response, = post('get_config', request_body)
|
110
148
|
return JSON.parse(response.body) unless response.nil?
|
111
149
|
|
112
150
|
nil
|
@@ -116,7 +154,7 @@ module Statsig
|
|
116
154
|
|
117
155
|
def post_logs(events)
|
118
156
|
json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
|
119
|
-
|
157
|
+
post('log_event', json_body, @post_logs_retry_limit)
|
120
158
|
rescue StandardError
|
121
159
|
|
122
160
|
end
|
data/lib/spec_store.rb
CHANGED
@@ -13,7 +13,7 @@ module Statsig
|
|
13
13
|
attr_accessor :initial_config_sync_time
|
14
14
|
attr_accessor :init_reason
|
15
15
|
|
16
|
-
def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
|
16
|
+
def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key)
|
17
17
|
@init_reason = EvaluationReason::UNINITIALIZED
|
18
18
|
@network = network
|
19
19
|
@options = options
|
@@ -35,6 +35,7 @@ module Statsig
|
|
35
35
|
@diagnostics = diagnostics
|
36
36
|
@error_boundary = error_boundary
|
37
37
|
@logger = logger
|
38
|
+
@secret_key = secret_key
|
38
39
|
|
39
40
|
@id_list_thread_pool = Concurrent::FixedThreadPool.new(
|
40
41
|
options.idlist_threadpool_size,
|
@@ -121,6 +122,18 @@ module Statsig
|
|
121
122
|
@specs[:layers][layer_name]
|
122
123
|
end
|
123
124
|
|
125
|
+
def gates
|
126
|
+
@specs[:gates]
|
127
|
+
end
|
128
|
+
|
129
|
+
def configs
|
130
|
+
@specs[:configs]
|
131
|
+
end
|
132
|
+
|
133
|
+
def layers
|
134
|
+
@specs[:layers]
|
135
|
+
end
|
136
|
+
|
124
137
|
def get_id_list(list_name)
|
125
138
|
@specs[:id_lists][list_name]
|
126
139
|
end
|
@@ -238,7 +251,7 @@ module Statsig
|
|
238
251
|
|
239
252
|
error = nil
|
240
253
|
begin
|
241
|
-
response, e = @network.
|
254
|
+
response, e = @network.download_config_specs(@last_config_sync_time)
|
242
255
|
code = response&.status.to_i
|
243
256
|
if e.is_a? NetworkError
|
244
257
|
code = e.http_code
|
@@ -276,6 +289,12 @@ module Statsig
|
|
276
289
|
specs_json = JSON.parse(specs_string)
|
277
290
|
return false unless specs_json.is_a? Hash
|
278
291
|
|
292
|
+
hashed_sdk_key_used = specs_json['hashed_sdk_key_used']
|
293
|
+
unless hashed_sdk_key_used.nil? or hashed_sdk_key_used == Statsig::HashUtils.djb2(@secret_key)
|
294
|
+
err_boundary.log_exception(Statsig::InvalidSDKKeyResponse.new)
|
295
|
+
return false
|
296
|
+
end
|
297
|
+
|
279
298
|
@last_config_sync_time = specs_json['time'] || @last_config_sync_time
|
280
299
|
return false unless specs_json['has_updates'] == true &&
|
281
300
|
!specs_json['feature_gates'].nil? &&
|
@@ -334,7 +353,7 @@ module Statsig
|
|
334
353
|
|
335
354
|
def get_id_lists_from_network
|
336
355
|
tracker = @diagnostics.track('get_id_list_sources', 'network_request')
|
337
|
-
response, e = @network.
|
356
|
+
response, e = @network.post('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
|
338
357
|
code = response&.status.to_i
|
339
358
|
if e.is_a? NetworkError
|
340
359
|
code = e.http_code
|
data/lib/statsig.rb
CHANGED
@@ -26,6 +26,23 @@ module Statsig
|
|
26
26
|
@shared_instance = StatsigDriver.new(secret_key, options, error_callback)
|
27
27
|
end
|
28
28
|
|
29
|
+
class GetGateOptions < T::Struct
|
30
|
+
prop :disable_log_exposure, T::Boolean, default: false
|
31
|
+
prop :skip_evaluation, T::Boolean, default: false
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { params(user: StatsigUser, gate_name: String, options: GetGateOptions).returns(FeatureGate) }
|
35
|
+
##
|
36
|
+
# Gets the gate, evaluated against the given user. An exposure event will automatically be logged for the gate.
|
37
|
+
#
|
38
|
+
# @param user A StatsigUser object used for the evaluation
|
39
|
+
# @param gate_name The name of the gate being checked
|
40
|
+
# @param options Additional options for evaluating the gate
|
41
|
+
def self.get_gate(user, gate_name, options = GetGateOptions.new)
|
42
|
+
ensure_initialized
|
43
|
+
@shared_instance&.get_gate(user, gate_name, options)
|
44
|
+
end
|
45
|
+
|
29
46
|
class CheckGateOptions < T::Struct
|
30
47
|
prop :disable_log_exposure, T::Boolean, default: false
|
31
48
|
end
|
@@ -212,6 +229,51 @@ module Statsig
|
|
212
229
|
@shared_instance&.manually_sync_idlists
|
213
230
|
end
|
214
231
|
|
232
|
+
sig { returns(T::Array[String]) }
|
233
|
+
##
|
234
|
+
# Returns a list of all gate names
|
235
|
+
#
|
236
|
+
def self.list_gates
|
237
|
+
ensure_initialized
|
238
|
+
@shared_instance&.list_gates
|
239
|
+
end
|
240
|
+
|
241
|
+
sig { returns(T::Array[String]) }
|
242
|
+
##
|
243
|
+
# Returns a list of all config names
|
244
|
+
#
|
245
|
+
def self.list_configs
|
246
|
+
ensure_initialized
|
247
|
+
@shared_instance&.list_configs
|
248
|
+
end
|
249
|
+
|
250
|
+
sig { returns(T::Array[String]) }
|
251
|
+
##
|
252
|
+
# Returns a list of all experiment names
|
253
|
+
#
|
254
|
+
def self.list_experiments
|
255
|
+
ensure_initialized
|
256
|
+
@shared_instance&.list_experiments
|
257
|
+
end
|
258
|
+
|
259
|
+
sig { returns(T::Array[String]) }
|
260
|
+
##
|
261
|
+
# Returns a list of all autotune names
|
262
|
+
#
|
263
|
+
def self.list_autotunes
|
264
|
+
ensure_initialized
|
265
|
+
@shared_instance&.list_autotunes
|
266
|
+
end
|
267
|
+
|
268
|
+
sig { returns(T::Array[String]) }
|
269
|
+
##
|
270
|
+
# Returns a list of all layer names
|
271
|
+
#
|
272
|
+
def self.list_layers
|
273
|
+
ensure_initialized
|
274
|
+
@shared_instance&.list_layers
|
275
|
+
end
|
276
|
+
|
215
277
|
sig { void }
|
216
278
|
##
|
217
279
|
# Stops all Statsig activity and flushes any pending events.
|
@@ -265,7 +327,8 @@ module Statsig
|
|
265
327
|
def self.get_statsig_metadata
|
266
328
|
{
|
267
329
|
'sdkType' => 'ruby-server',
|
268
|
-
'sdkVersion' => '1.
|
330
|
+
'sdkVersion' => '1.30.0',
|
331
|
+
'languageVersion' => RUBY_VERSION
|
269
332
|
}
|
270
333
|
end
|
271
334
|
|
data/lib/statsig_driver.rb
CHANGED
@@ -10,6 +10,7 @@ require 'statsig_options'
|
|
10
10
|
require 'statsig_user'
|
11
11
|
require 'spec_store'
|
12
12
|
require 'dynamic_config'
|
13
|
+
require 'feature_gate'
|
13
14
|
require 'error_boundary'
|
14
15
|
require 'layer'
|
15
16
|
require 'sorbet-runtime'
|
@@ -38,34 +39,62 @@ class StatsigDriver
|
|
38
39
|
@net = Statsig::Network.new(secret_key, @options)
|
39
40
|
@logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
|
40
41
|
@persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
|
41
|
-
@
|
42
|
+
@store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key)
|
43
|
+
@evaluator = Statsig::Evaluator.new(@store, @options, @persistent_storage_utils)
|
42
44
|
tracker.end(success: true)
|
43
45
|
|
44
46
|
@logger.log_diagnostics_event(@diagnostics)
|
45
47
|
}, caller: __method__.to_s)
|
46
48
|
end
|
47
49
|
|
50
|
+
sig do
|
51
|
+
params(
|
52
|
+
user: StatsigUser,
|
53
|
+
gate_name: String,
|
54
|
+
disable_log_exposure: T::Boolean,
|
55
|
+
skip_evaluation: T::Boolean
|
56
|
+
).returns(FeatureGate)
|
57
|
+
end
|
58
|
+
def get_gate_impl(user, gate_name, disable_log_exposure: false, skip_evaluation: false)
|
59
|
+
if skip_evaluation
|
60
|
+
gate = @store.get_gate(gate_name)
|
61
|
+
return FeatureGate.new(gate_name) if gate.nil?
|
62
|
+
return FeatureGate.new(gate['name'], target_app_ids: gate['targetAppIDs'])
|
63
|
+
end
|
64
|
+
user = verify_inputs(user, gate_name, 'gate_name')
|
65
|
+
|
66
|
+
res = @evaluator.check_gate(user, gate_name)
|
67
|
+
if res.nil?
|
68
|
+
res = Statsig::ConfigResult.new(gate_name)
|
69
|
+
end
|
70
|
+
|
71
|
+
if res == $fetch_from_server
|
72
|
+
res = check_gate_fallback(user, gate_name)
|
73
|
+
# exposure logged by the server
|
74
|
+
else
|
75
|
+
unless disable_log_exposure
|
76
|
+
@logger.log_gate_exposure(
|
77
|
+
user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
FeatureGate.from_config_result(res)
|
82
|
+
end
|
83
|
+
|
84
|
+
sig { params(user: StatsigUser, gate_name: String, options: Statsig::GetGateOptions).returns(FeatureGate) }
|
85
|
+
def get_gate(user, gate_name, options = Statsig::GetGateOptions.new)
|
86
|
+
@err_boundary.capture(task: lambda {
|
87
|
+
run_with_diagnostics(task: lambda {
|
88
|
+
get_gate_impl(user, gate_name, disable_log_exposure: options.disable_log_exposure, skip_evaluation: options.skip_evaluation)
|
89
|
+
}, caller: __method__.to_s)
|
90
|
+
}, recover: -> { false }, caller: __method__.to_s)
|
91
|
+
end
|
92
|
+
|
48
93
|
sig { params(user: StatsigUser, gate_name: String, options: Statsig::CheckGateOptions).returns(T::Boolean) }
|
49
94
|
def check_gate(user, gate_name, options = Statsig::CheckGateOptions.new)
|
50
95
|
@err_boundary.capture(task: lambda {
|
51
96
|
run_with_diagnostics(task: lambda {
|
52
|
-
|
53
|
-
|
54
|
-
res = @evaluator.check_gate(user, gate_name)
|
55
|
-
if res.nil?
|
56
|
-
res = Statsig::ConfigResult.new(gate_name)
|
57
|
-
end
|
58
|
-
|
59
|
-
if res == $fetch_from_server
|
60
|
-
res = check_gate_fallback(user, gate_name)
|
61
|
-
# exposure logged by the server
|
62
|
-
else
|
63
|
-
if !options.disable_log_exposure
|
64
|
-
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
res.gate_value
|
97
|
+
get_gate_impl(user, gate_name, disable_log_exposure: options.disable_log_exposure).value
|
69
98
|
}, caller: __method__.to_s)
|
70
99
|
}, recover: -> { false }, caller: __method__.to_s)
|
71
100
|
end
|
@@ -130,7 +159,7 @@ class StatsigDriver
|
|
130
159
|
end
|
131
160
|
|
132
161
|
if res == $fetch_from_server
|
133
|
-
if res.config_delegate.
|
162
|
+
if res.config_delegate.nil?
|
134
163
|
return Layer.new(layer_name)
|
135
164
|
end
|
136
165
|
res = get_config_fallback(user, res.config_delegate)
|
@@ -140,7 +169,7 @@ class StatsigDriver
|
|
140
169
|
exposure_log_func = !options.disable_log_exposure ? lambda { |layer, parameter_name|
|
141
170
|
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
142
171
|
} : nil
|
143
|
-
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
172
|
+
Layer.new(res.name, res.json_value, res.rule_id, res.group_name, res.config_delegate, exposure_log_func)
|
144
173
|
}, caller: __method__.to_s)
|
145
174
|
}, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
|
146
175
|
end
|
@@ -149,7 +178,7 @@ class StatsigDriver
|
|
149
178
|
def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
150
179
|
@err_boundary.capture(task: lambda {
|
151
180
|
res = @evaluator.get_layer(user, layer_name)
|
152
|
-
layer = Layer.new(layer_name, res.json_value, res.rule_id)
|
181
|
+
layer = Layer.new(layer_name, res.json_value, res.rule_id, res.group_name, res.config_delegate)
|
153
182
|
context = { 'is_manual_exposure' => true }
|
154
183
|
@logger.log_layer_exposure(user, layer, parameter_name, res, context)
|
155
184
|
}, caller: __method__.to_s)
|
@@ -184,6 +213,41 @@ class StatsigDriver
|
|
184
213
|
}, caller: __method__.to_s)
|
185
214
|
end
|
186
215
|
|
216
|
+
sig { returns(T::Array[String]) }
|
217
|
+
def list_gates
|
218
|
+
@err_boundary.capture(task: lambda {
|
219
|
+
@evaluator.list_gates
|
220
|
+
}, caller: __method__.to_s)
|
221
|
+
end
|
222
|
+
|
223
|
+
sig { returns(T::Array[String]) }
|
224
|
+
def list_configs
|
225
|
+
@err_boundary.capture(task: lambda {
|
226
|
+
@evaluator.list_configs
|
227
|
+
}, caller: __method__.to_s)
|
228
|
+
end
|
229
|
+
|
230
|
+
sig { returns(T::Array[String]) }
|
231
|
+
def list_experiments
|
232
|
+
@err_boundary.capture(task: lambda {
|
233
|
+
@evaluator.list_experiments
|
234
|
+
}, caller: __method__.to_s)
|
235
|
+
end
|
236
|
+
|
237
|
+
sig { returns(T::Array[String]) }
|
238
|
+
def list_autotunes
|
239
|
+
@err_boundary.capture(task: lambda {
|
240
|
+
@evaluator.list_autotunes
|
241
|
+
}, caller: __method__.to_s)
|
242
|
+
end
|
243
|
+
|
244
|
+
sig { returns(T::Array[String]) }
|
245
|
+
def list_layers
|
246
|
+
@err_boundary.capture(task: lambda {
|
247
|
+
@evaluator.list_layers
|
248
|
+
}, caller: __method__.to_s)
|
249
|
+
end
|
250
|
+
|
187
251
|
def shutdown
|
188
252
|
@err_boundary.capture(task: lambda {
|
189
253
|
@shutdown = true
|
data/lib/statsig_errors.rb
CHANGED
data/lib/statsig_options.rb
CHANGED
@@ -22,7 +22,7 @@ class StatsigOptions
|
|
22
22
|
|
23
23
|
# The base url used specifically to call download_config_specs.
|
24
24
|
# Takes precedence over api_url_base
|
25
|
-
sig { returns(
|
25
|
+
sig { returns(String) }
|
26
26
|
attr_accessor :api_url_download_config_specs
|
27
27
|
|
28
28
|
sig { returns(T.any(Float, Integer)) }
|
@@ -115,7 +115,7 @@ class StatsigOptions
|
|
115
115
|
sig do
|
116
116
|
params(
|
117
117
|
environment: T.any(T::Hash[String, String], NilClass),
|
118
|
-
api_url_base: String,
|
118
|
+
api_url_base: T.nilable(String),
|
119
119
|
api_url_download_config_specs: T.any(String, NilClass),
|
120
120
|
rulesets_sync_interval: T.any(Float, Integer),
|
121
121
|
idlists_sync_interval: T.any(Float, Integer),
|
@@ -137,10 +137,9 @@ class StatsigOptions
|
|
137
137
|
user_persistent_storage: T.any(Statsig::Interfaces::IUserPersistentStorage, NilClass)
|
138
138
|
).void
|
139
139
|
end
|
140
|
-
|
141
140
|
def initialize(
|
142
141
|
environment = nil,
|
143
|
-
api_url_base =
|
142
|
+
api_url_base = nil,
|
144
143
|
api_url_download_config_specs: nil,
|
145
144
|
rulesets_sync_interval: 10,
|
146
145
|
idlists_sync_interval: 60,
|
@@ -162,8 +161,8 @@ class StatsigOptions
|
|
162
161
|
user_persistent_storage: nil
|
163
162
|
)
|
164
163
|
@environment = environment.is_a?(Hash) ? environment : nil
|
165
|
-
@api_url_base = api_url_base
|
166
|
-
@api_url_download_config_specs = api_url_download_config_specs
|
164
|
+
@api_url_base = api_url_base || 'https://statsigapi.net/v1'
|
165
|
+
@api_url_download_config_specs = api_url_download_config_specs || api_url_base || 'https://api.statsigcdn.com/v1'
|
167
166
|
@rulesets_sync_interval = rulesets_sync_interval
|
168
167
|
@idlists_sync_interval = idlists_sync_interval
|
169
168
|
@disable_rulesets_sync = disable_rulesets_sync
|
data/lib/uri_helper.rb
CHANGED
@@ -17,7 +17,7 @@ class URIHelper
|
|
17
17
|
sig { params(endpoint: String).returns(String) }
|
18
18
|
def build_url(endpoint)
|
19
19
|
api = @options.api_url_base
|
20
|
-
if endpoint
|
20
|
+
if endpoint.include?('download_config_specs')
|
21
21
|
api = T.must(@options.api_url_download_config_specs)
|
22
22
|
end
|
23
23
|
unless api.end_with?('/')
|
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.30.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Statsig, Inc
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -330,6 +330,7 @@ files:
|
|
330
330
|
- lib/evaluation_details.rb
|
331
331
|
- lib/evaluation_helpers.rb
|
332
332
|
- lib/evaluator.rb
|
333
|
+
- lib/feature_gate.rb
|
333
334
|
- lib/hash_utils.rb
|
334
335
|
- lib/id_list.rb
|
335
336
|
- lib/interfaces/data_store.rb
|
@@ -351,7 +352,7 @@ homepage: https://rubygems.org/gems/statsig
|
|
351
352
|
licenses:
|
352
353
|
- ISC
|
353
354
|
metadata: {}
|
354
|
-
post_install_message:
|
355
|
+
post_install_message:
|
355
356
|
rdoc_options: []
|
356
357
|
require_paths:
|
357
358
|
- lib
|
@@ -362,12 +363,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
362
363
|
version: 2.5.0
|
363
364
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
364
365
|
requirements:
|
365
|
-
- - "
|
366
|
+
- - ">="
|
366
367
|
- !ruby/object:Gem::Version
|
367
|
-
version:
|
368
|
+
version: '0'
|
368
369
|
requirements: []
|
369
|
-
rubygems_version: 3.
|
370
|
-
signing_key:
|
370
|
+
rubygems_version: 3.2.33
|
371
|
+
signing_key:
|
371
372
|
specification_version: 4
|
372
373
|
summary: Statsig server SDK for Ruby
|
373
374
|
test_files: []
|