statsig 1.25.0 → 1.25.2
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 +67 -8
- data/lib/error_boundary.rb +35 -14
- data/lib/evaluator.rb +2 -2
- data/lib/network.rb +5 -7
- data/lib/spec_store.rb +58 -40
- data/lib/statsig.rb +1 -1
- data/lib/statsig_driver.rb +23 -31
- data/lib/statsig_logger.rb +4 -0
- data/lib/statsig_options.rb +8 -0
- data/lib/uri_helper.rb +37 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7f3c8522d7d8062daf248fd667fd0d8152a4194339fde799814a5da44812eda
|
4
|
+
data.tar.gz: 1fd5e67d4a75ab3b5b6c92c780841252a4610e18bc13310f9b11dbb583aaf16a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f516ca6d221c9d789f1f358b550edeedf4203c01ead45374b97a3b81f4205d09f1c7c3e0214516cdcad314bacbdbde39a5df2053779679fe4df593c974c8300
|
7
|
+
data.tar.gz: a90846ef3c050c7c0b8bddbabd19ed7bff6236a15ed941f55d1a305f4c9cb5ff35f595a3d7ea4ab1560d7f7bae11c669c29d17f8041da9125524669703dc5370
|
data/lib/diagnostics.rb
CHANGED
@@ -12,33 +12,92 @@ module Statsig
|
|
12
12
|
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
13
13
|
attr_reader :markers
|
14
14
|
|
15
|
-
sig { params(context: String).void }
|
16
|
-
|
17
15
|
def initialize(context)
|
18
16
|
@context = context
|
19
17
|
@markers = []
|
20
18
|
end
|
21
19
|
|
22
|
-
sig
|
20
|
+
sig do
|
21
|
+
params(
|
22
|
+
key: String,
|
23
|
+
action: String,
|
24
|
+
step: T.any(String, NilClass),
|
25
|
+
value: T.any(String, Integer, T::Boolean, NilClass),
|
26
|
+
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
27
|
+
).void
|
28
|
+
end
|
23
29
|
|
24
|
-
def mark(key, action, step = nil, value = nil)
|
30
|
+
def mark(key, action, step = nil, value = nil, metadata = nil)
|
25
31
|
@markers.push({
|
26
32
|
key: key,
|
27
33
|
step: step,
|
28
34
|
action: action,
|
29
35
|
value: value,
|
36
|
+
metadata: metadata,
|
30
37
|
timestamp: (Time.now.to_f * 1000).to_i
|
31
38
|
})
|
32
39
|
end
|
33
40
|
|
41
|
+
sig do
|
42
|
+
params(
|
43
|
+
key: String,
|
44
|
+
step: T.any(String, NilClass),
|
45
|
+
value: T.any(String, Integer, T::Boolean, NilClass),
|
46
|
+
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
47
|
+
).returns(Tracker)
|
48
|
+
end
|
49
|
+
def track(key, step = nil, value = nil, metadata = nil)
|
50
|
+
tracker = Tracker.new(self, key, step, metadata)
|
51
|
+
tracker.start(value)
|
52
|
+
tracker
|
53
|
+
end
|
54
|
+
|
34
55
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
35
56
|
|
36
57
|
def serialize
|
37
58
|
{
|
38
|
-
context: @context,
|
39
|
-
markers: @markers
|
59
|
+
context: @context.clone,
|
60
|
+
markers: @markers.clone
|
40
61
|
}
|
41
62
|
end
|
42
|
-
end
|
43
63
|
|
44
|
-
|
64
|
+
def clear_markers
|
65
|
+
@markers.clear
|
66
|
+
end
|
67
|
+
|
68
|
+
class Context
|
69
|
+
INITIALIZE = 'initialize'.freeze
|
70
|
+
CONFIG_SYNC = 'config_sync'.freeze
|
71
|
+
API_CALL = 'api_call'.freeze
|
72
|
+
end
|
73
|
+
|
74
|
+
API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
|
75
|
+
|
76
|
+
class Tracker
|
77
|
+
extend T::Sig
|
78
|
+
|
79
|
+
sig do
|
80
|
+
params(
|
81
|
+
diagnostics: Diagnostics,
|
82
|
+
key: String,
|
83
|
+
step: T.any(String, NilClass),
|
84
|
+
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
85
|
+
).void
|
86
|
+
end
|
87
|
+
def initialize(diagnostics, key, step, metadata)
|
88
|
+
@diagnostics = diagnostics
|
89
|
+
@key = key
|
90
|
+
@step = step
|
91
|
+
@metadata = metadata
|
92
|
+
end
|
93
|
+
|
94
|
+
def start(value = nil)
|
95
|
+
@diagnostics.mark(@key, 'start', @step, value, @metadata)
|
96
|
+
end
|
97
|
+
|
98
|
+
def end(value = nil)
|
99
|
+
@diagnostics.mark(@key, 'end', @step, value, @metadata)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/error_boundary.rb
CHANGED
@@ -1,25 +1,46 @@
|
|
1
|
-
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'statsig_errors'
|
4
|
+
require 'sorbet-runtime'
|
2
5
|
|
3
6
|
$endpoint = 'https://statsigapi.net/v1/sdk_exception'
|
4
7
|
|
5
8
|
module Statsig
|
6
9
|
class ErrorBoundary
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(T.any(StatsigLogger, NilClass)) }
|
13
|
+
attr_accessor :logger
|
14
|
+
|
15
|
+
sig { params(sdk_key: String).void }
|
7
16
|
def initialize(sdk_key)
|
8
17
|
@sdk_key = sdk_key
|
9
18
|
@seen = Set.new
|
10
19
|
end
|
11
20
|
|
12
|
-
def
|
21
|
+
def sample_diagnostics
|
22
|
+
rand(10_000).zero?
|
23
|
+
end
|
24
|
+
|
25
|
+
def capture(task:, recover: -> {}, caller: nil)
|
26
|
+
if !caller.nil? && Diagnostics::API_CALL_KEYS.include?(caller) && sample_diagnostics
|
27
|
+
diagnostics = Diagnostics.new('api_call')
|
28
|
+
tracker = diagnostics.track(caller)
|
29
|
+
end
|
13
30
|
begin
|
14
|
-
|
31
|
+
res = task.call
|
32
|
+
tracker&.end(true)
|
15
33
|
rescue StandardError => e
|
34
|
+
tracker&.end(false)
|
16
35
|
if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
|
17
36
|
raise e
|
18
37
|
end
|
19
|
-
puts
|
38
|
+
puts '[Statsig]: An unexpected exception occurred.'
|
20
39
|
log_exception(e)
|
21
|
-
|
40
|
+
res = recover.call
|
22
41
|
end
|
42
|
+
@logger&.log_diagnostics_event(diagnostics)
|
43
|
+
return res
|
23
44
|
end
|
24
45
|
|
25
46
|
private
|
@@ -35,18 +56,18 @@ module Statsig
|
|
35
56
|
meta = Statsig.get_statsig_metadata
|
36
57
|
http = HTTP.headers(
|
37
58
|
{
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
59
|
+
'STATSIG-API-KEY' => @sdk_key,
|
60
|
+
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
61
|
+
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
62
|
+
'Content-Type' => 'application/json; charset=UTF-8'
|
42
63
|
}).accept(:json)
|
43
64
|
body = {
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
65
|
+
'exception' => name,
|
66
|
+
'info' => {
|
67
|
+
'trace' => exception.backtrace.to_s,
|
68
|
+
'message' => exception.message
|
48
69
|
}.to_s,
|
49
|
-
|
70
|
+
'statsigMetadata' => meta
|
50
71
|
}
|
51
72
|
http.post($endpoint, body: JSON.generate(body))
|
52
73
|
rescue
|
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, diagnostics)
|
21
|
+
@spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics)
|
22
22
|
UAParser.initialize_async
|
23
23
|
CountryLookup.initialize_async
|
24
24
|
|
data/lib/network.rb
CHANGED
@@ -4,6 +4,7 @@ require 'http'
|
|
4
4
|
require 'json'
|
5
5
|
require 'securerandom'
|
6
6
|
require 'sorbet-runtime'
|
7
|
+
require 'uri_helper'
|
7
8
|
|
8
9
|
$retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
|
9
10
|
|
@@ -24,12 +25,8 @@ module Statsig
|
|
24
25
|
|
25
26
|
def initialize(server_secret, options, backoff_mult = 10)
|
26
27
|
super()
|
27
|
-
|
28
|
-
unless api.end_with?('/')
|
29
|
-
api += '/'
|
30
|
-
end
|
28
|
+
URIHelper.initialize(options)
|
31
29
|
@server_secret = server_secret
|
32
|
-
@api = api
|
33
30
|
@local_mode = options.local_mode
|
34
31
|
@timeout = options.network_timeout
|
35
32
|
@backoff_multiplier = backoff_mult
|
@@ -67,8 +64,9 @@ module Statsig
|
|
67
64
|
backoff_adjusted = @post_logs_retry_backoff.call(retries)
|
68
65
|
end
|
69
66
|
end
|
67
|
+
url = URIHelper.build_url(endpoint)
|
70
68
|
begin
|
71
|
-
res = http.post(
|
69
|
+
res = http.post(url, body: body)
|
72
70
|
rescue StandardError => e
|
73
71
|
## network error retry
|
74
72
|
return nil, e unless retries > 0
|
@@ -76,7 +74,7 @@ module Statsig
|
|
76
74
|
return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
77
75
|
end
|
78
76
|
return res, nil if res.status.success?
|
79
|
-
return nil, NetworkError.new("Got an exception when making request to #{
|
77
|
+
return nil, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
|
80
78
|
## status code retry
|
81
79
|
sleep backoff_adjusted
|
82
80
|
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
data/lib/spec_store.rb
CHANGED
@@ -12,7 +12,7 @@ module Statsig
|
|
12
12
|
attr_accessor :initial_config_sync_time
|
13
13
|
attr_accessor :init_reason
|
14
14
|
|
15
|
-
def initialize(network, options, error_callback,
|
15
|
+
def initialize(network, options, error_callback, diagnostics)
|
16
16
|
@init_reason = EvaluationReason::UNINITIALIZED
|
17
17
|
@network = network
|
18
18
|
@options = options
|
@@ -29,6 +29,7 @@ module Statsig
|
|
29
29
|
:id_lists => {},
|
30
30
|
:experiment_to_layer => {}
|
31
31
|
}
|
32
|
+
@diagnostics = diagnostics
|
32
33
|
|
33
34
|
@id_list_thread_pool = Concurrent::FixedThreadPool.new(
|
34
35
|
options.idlist_threadpool_size,
|
@@ -42,11 +43,11 @@ module Statsig
|
|
42
43
|
if !@options.data_store.nil?
|
43
44
|
puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
|
44
45
|
else
|
45
|
-
|
46
|
+
tracker = @diagnostics.track('bootstrap', 'process')
|
46
47
|
if process_specs(options.bootstrap_values)
|
47
48
|
@init_reason = EvaluationReason::BOOTSTRAP
|
48
49
|
end
|
49
|
-
|
50
|
+
tracker.end(@init_reason == EvaluationReason::BOOTSTRAP)
|
50
51
|
end
|
51
52
|
rescue
|
52
53
|
puts 'the provided bootstrapValues is not a valid JSON string'
|
@@ -54,21 +55,19 @@ module Statsig
|
|
54
55
|
end
|
55
56
|
|
56
57
|
unless @options.data_store.nil?
|
57
|
-
init_diagnostics&.mark("data_store", "start", "load")
|
58
58
|
@options.data_store.init
|
59
|
-
load_config_specs_from_storage_adapter
|
60
|
-
init_diagnostics&.mark("data_store", "end", "load", @init_reason == EvaluationReason::DATA_ADAPTER)
|
59
|
+
load_config_specs_from_storage_adapter
|
61
60
|
end
|
62
61
|
|
63
62
|
if @init_reason == EvaluationReason::UNINITIALIZED
|
64
|
-
download_config_specs
|
63
|
+
download_config_specs
|
65
64
|
end
|
66
65
|
|
67
66
|
@initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
|
68
67
|
if !@options.data_store.nil?
|
69
|
-
get_id_lists_from_adapter
|
68
|
+
get_id_lists_from_adapter
|
70
69
|
else
|
71
|
-
get_id_lists_from_network
|
70
|
+
get_id_lists_from_network
|
72
71
|
end
|
73
72
|
|
74
73
|
@config_sync_thread = sync_config_specs
|
@@ -135,20 +134,20 @@ module Statsig
|
|
135
134
|
|
136
135
|
private
|
137
136
|
|
138
|
-
def load_config_specs_from_storage_adapter
|
139
|
-
|
137
|
+
def load_config_specs_from_storage_adapter
|
138
|
+
tracker = @diagnostics.track('data_store_config_specs', 'fetch')
|
140
139
|
cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
141
|
-
|
140
|
+
tracker.end(true)
|
142
141
|
return if cached_values.nil?
|
143
142
|
|
144
|
-
|
143
|
+
tracker = @diagnostics.track('data_store_config_specs', 'process')
|
145
144
|
process_specs(cached_values, from_adapter: true)
|
146
145
|
@init_reason = EvaluationReason::DATA_ADAPTER
|
147
|
-
|
146
|
+
tracker.end(true)
|
148
147
|
rescue StandardError
|
149
148
|
# Fallback to network
|
150
|
-
|
151
|
-
download_config_specs
|
149
|
+
tracker.end(false)
|
150
|
+
download_config_specs
|
152
151
|
end
|
153
152
|
|
154
153
|
def save_config_specs_to_storage_adapter(specs_string)
|
@@ -160,6 +159,7 @@ module Statsig
|
|
160
159
|
|
161
160
|
def sync_config_specs
|
162
161
|
Thread.new do
|
162
|
+
@diagnostics = Diagnostics.new('config_sync')
|
163
163
|
loop do
|
164
164
|
sleep @options.rulesets_sync_interval
|
165
165
|
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
@@ -173,6 +173,7 @@ module Statsig
|
|
173
173
|
|
174
174
|
def sync_id_lists
|
175
175
|
Thread.new do
|
176
|
+
@diagnostics = Diagnostics.new('config_sync')
|
176
177
|
loop do
|
177
178
|
sleep @id_lists_sync_interval
|
178
179
|
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
|
@@ -184,8 +185,8 @@ module Statsig
|
|
184
185
|
end
|
185
186
|
end
|
186
187
|
|
187
|
-
def download_config_specs
|
188
|
-
|
188
|
+
def download_config_specs
|
189
|
+
tracker = @diagnostics.track('download_config_specs', 'network_request')
|
189
190
|
|
190
191
|
error = nil
|
191
192
|
begin
|
@@ -194,18 +195,17 @@ module Statsig
|
|
194
195
|
if e.is_a? NetworkError
|
195
196
|
code = e.http_code
|
196
197
|
end
|
197
|
-
|
198
|
+
tracker.end(code)
|
198
199
|
|
199
200
|
if e.nil?
|
200
201
|
unless response.nil?
|
201
|
-
|
202
|
-
|
202
|
+
tracker = @diagnostics.track('download_config_specs', 'process')
|
203
203
|
if process_specs(response.body.to_s)
|
204
204
|
@init_reason = EvaluationReason::NETWORK
|
205
|
-
@rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
|
206
205
|
end
|
206
|
+
tracker.end(@init_reason == EvaluationReason::NETWORK)
|
207
207
|
|
208
|
-
|
208
|
+
@rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
|
209
209
|
end
|
210
210
|
|
211
211
|
nil
|
@@ -259,18 +259,18 @@ module Statsig
|
|
259
259
|
true
|
260
260
|
end
|
261
261
|
|
262
|
-
def get_id_lists_from_adapter
|
263
|
-
|
262
|
+
def get_id_lists_from_adapter
|
263
|
+
tracker = @diagnostics.track('data_store_id_lists', 'fetch')
|
264
264
|
cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
|
265
265
|
return if cached_values.nil?
|
266
266
|
|
267
|
-
|
267
|
+
tracker.end(true)
|
268
268
|
id_lists = JSON.parse(cached_values)
|
269
|
-
process_id_lists(id_lists,
|
269
|
+
process_id_lists(id_lists, from_adapter: true)
|
270
270
|
rescue StandardError
|
271
271
|
# Fallback to network
|
272
|
-
|
273
|
-
get_id_lists_from_network
|
272
|
+
tracker.end(false)
|
273
|
+
get_id_lists_from_network
|
274
274
|
end
|
275
275
|
|
276
276
|
def save_id_lists_to_adapter(id_lists_raw_json)
|
@@ -280,36 +280,45 @@ module Statsig
|
|
280
280
|
@options.data_store.set(Interfaces::IDataStore::ID_LISTS_KEY, id_lists_raw_json)
|
281
281
|
end
|
282
282
|
|
283
|
-
def get_id_lists_from_network
|
284
|
-
|
283
|
+
def get_id_lists_from_network
|
284
|
+
tracker = @diagnostics.track('get_id_list_sources', 'network_request')
|
285
285
|
response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
|
286
|
+
code = response&.status.to_i
|
287
|
+
if e.is_a? NetworkError
|
288
|
+
code = e.http_code
|
289
|
+
end
|
290
|
+
tracker.end(code)
|
286
291
|
if !e.nil? || response.nil?
|
287
292
|
return
|
288
293
|
end
|
289
|
-
init_diagnostics&.mark("get_id_lists", "end", "network_request", response.status.to_i)
|
290
294
|
|
291
295
|
begin
|
292
296
|
server_id_lists = JSON.parse(response)
|
293
|
-
process_id_lists(server_id_lists
|
297
|
+
process_id_lists(server_id_lists)
|
294
298
|
save_id_lists_to_adapter(response.body.to_s)
|
295
299
|
rescue
|
296
300
|
# Ignored, will try again
|
297
301
|
end
|
298
302
|
end
|
299
303
|
|
300
|
-
def process_id_lists(new_id_lists,
|
304
|
+
def process_id_lists(new_id_lists, from_adapter: false)
|
301
305
|
local_id_lists = @specs[:id_lists]
|
302
306
|
if !new_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
|
303
307
|
return
|
304
308
|
end
|
305
309
|
tasks = []
|
306
310
|
|
307
|
-
|
311
|
+
tracker = @diagnostics.track(
|
312
|
+
from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
|
313
|
+
'process',
|
314
|
+
new_id_lists.length
|
315
|
+
)
|
316
|
+
|
317
|
+
if new_id_lists.empty?
|
318
|
+
tracker.end
|
308
319
|
return
|
309
320
|
end
|
310
321
|
|
311
|
-
init_diagnostics&.mark("get_id_lists", "start", "process", new_id_lists.length)
|
312
|
-
|
313
322
|
delete_lists = []
|
314
323
|
local_id_lists.each do |list_name, list|
|
315
324
|
unless new_id_lists.key? list_name
|
@@ -357,14 +366,17 @@ module Statsig
|
|
357
366
|
end
|
358
367
|
|
359
368
|
result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
|
360
|
-
|
369
|
+
tracker.end(result.state == :fulfilled)
|
361
370
|
end
|
362
371
|
|
363
372
|
def get_single_id_list_from_adapter(list)
|
373
|
+
tracker = @diagnostics.track('data_store_id_list', 'fetch', nil, { url: list.url })
|
364
374
|
cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
|
375
|
+
tracker.end(true)
|
365
376
|
content = cached_values.to_s
|
366
|
-
process_single_id_list(list, content)
|
377
|
+
process_single_id_list(list, content, from_adapter: true)
|
367
378
|
rescue StandardError
|
379
|
+
tracker.end(false)
|
368
380
|
nil
|
369
381
|
end
|
370
382
|
|
@@ -378,7 +390,9 @@ module Statsig
|
|
378
390
|
nil unless list.is_a? IDList
|
379
391
|
http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
|
380
392
|
begin
|
393
|
+
tracker = @diagnostics.track('get_id_list', 'network_request', nil, { url: list.url })
|
381
394
|
res = http.get(list.url)
|
395
|
+
tracker.end(res.status.code)
|
382
396
|
nil unless res.status.success?
|
383
397
|
content_length = Integer(res['content-length'])
|
384
398
|
nil if content_length.nil? || content_length <= 0
|
@@ -390,11 +404,13 @@ module Statsig
|
|
390
404
|
end
|
391
405
|
end
|
392
406
|
|
393
|
-
def process_single_id_list(list, content, content_length = nil)
|
407
|
+
def process_single_id_list(list, content, content_length = nil, from_adapter: false)
|
394
408
|
false unless list.is_a? IDList
|
395
409
|
begin
|
410
|
+
tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', nil, { url: list.url })
|
396
411
|
unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
|
397
412
|
@specs[:id_lists].delete(list.name)
|
413
|
+
tracker.end(false)
|
398
414
|
return false
|
399
415
|
end
|
400
416
|
ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
|
@@ -416,8 +432,10 @@ module Statsig
|
|
416
432
|
else
|
417
433
|
list.size + content_length
|
418
434
|
end
|
435
|
+
tracker.end(true)
|
419
436
|
return true
|
420
437
|
rescue
|
438
|
+
tracker.end(false)
|
421
439
|
return false
|
422
440
|
end
|
423
441
|
end
|
data/lib/statsig.rb
CHANGED
data/lib/statsig_driver.rb
CHANGED
@@ -30,19 +30,20 @@ class StatsigDriver
|
|
30
30
|
end
|
31
31
|
|
32
32
|
@err_boundary = Statsig::ErrorBoundary.new(secret_key)
|
33
|
-
@err_boundary.capture(
|
34
|
-
@
|
35
|
-
@
|
33
|
+
@err_boundary.capture(task: lambda {
|
34
|
+
@diagnostics = Statsig::Diagnostics.new('initialize')
|
35
|
+
tracker = @diagnostics.track('overall')
|
36
36
|
@options = options || StatsigOptions.new
|
37
37
|
@shutdown = false
|
38
38
|
@secret_key = secret_key
|
39
39
|
@net = Statsig::Network.new(secret_key, @options)
|
40
40
|
@logger = Statsig::StatsigLogger.new(@net, @options)
|
41
|
-
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @
|
42
|
-
|
41
|
+
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics)
|
42
|
+
tracker.end('success')
|
43
43
|
|
44
|
-
|
44
|
+
@logger.log_diagnostics_event(@diagnostics)
|
45
45
|
})
|
46
|
+
@err_boundary.logger = @logger
|
46
47
|
end
|
47
48
|
|
48
49
|
class CheckGateOptions < T::Struct
|
@@ -52,7 +53,7 @@ class StatsigDriver
|
|
52
53
|
sig { params(user: StatsigUser, gate_name: String, options: CheckGateOptions).returns(T::Boolean) }
|
53
54
|
|
54
55
|
def check_gate(user, gate_name, options = CheckGateOptions.new)
|
55
|
-
@err_boundary.capture(
|
56
|
+
@err_boundary.capture(task: lambda {
|
56
57
|
user = verify_inputs(user, gate_name, "gate_name")
|
57
58
|
|
58
59
|
res = @evaluator.check_gate(user, gate_name)
|
@@ -70,8 +71,7 @@ class StatsigDriver
|
|
70
71
|
end
|
71
72
|
|
72
73
|
res.gate_value
|
73
|
-
}, -> { false })
|
74
|
-
|
74
|
+
}, recover: -> { false }, caller: __method__.to_s)
|
75
75
|
end
|
76
76
|
|
77
77
|
sig { params(user: StatsigUser, gate_name: String).void }
|
@@ -89,10 +89,10 @@ class StatsigDriver
|
|
89
89
|
sig { params(user: StatsigUser, dynamic_config_name: String, options: GetConfigOptions).returns(DynamicConfig) }
|
90
90
|
|
91
91
|
def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
|
92
|
-
@err_boundary.capture(
|
92
|
+
@err_boundary.capture(task: lambda {
|
93
93
|
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
94
94
|
get_config_impl(user, dynamic_config_name, options)
|
95
|
-
}, -> { DynamicConfig.new(dynamic_config_name) })
|
95
|
+
}, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
|
96
96
|
end
|
97
97
|
|
98
98
|
class GetExperimentOptions < T::Struct
|
@@ -102,10 +102,10 @@ class StatsigDriver
|
|
102
102
|
sig { params(user: StatsigUser, experiment_name: String, options: GetExperimentOptions).returns(DynamicConfig) }
|
103
103
|
|
104
104
|
def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
|
105
|
-
@err_boundary.capture(
|
105
|
+
@err_boundary.capture(task: lambda {
|
106
106
|
user = verify_inputs(user, experiment_name, "experiment_name")
|
107
107
|
get_config_impl(user, experiment_name, options)
|
108
|
-
}, -> { DynamicConfig.new(experiment_name) })
|
108
|
+
}, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
|
109
109
|
end
|
110
110
|
|
111
111
|
sig { params(user: StatsigUser, config_name: String).void }
|
@@ -123,7 +123,7 @@ class StatsigDriver
|
|
123
123
|
sig { params(user: StatsigUser, layer_name: String, options: GetLayerOptions).returns(Layer) }
|
124
124
|
|
125
125
|
def get_layer(user, layer_name, options = GetLayerOptions.new)
|
126
|
-
@err_boundary.capture(
|
126
|
+
@err_boundary.capture(task: lambda {
|
127
127
|
user = verify_inputs(user, layer_name, "layer_name")
|
128
128
|
|
129
129
|
res = @evaluator.get_layer(user, layer_name)
|
@@ -143,9 +143,9 @@ class StatsigDriver
|
|
143
143
|
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
144
144
|
} : nil
|
145
145
|
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
146
|
-
},
|
146
|
+
}, recover: lambda {
|
147
147
|
Layer.new(layer_name)
|
148
|
-
})
|
148
|
+
}, caller: __method__.to_s)
|
149
149
|
end
|
150
150
|
|
151
151
|
sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
|
@@ -158,7 +158,7 @@ class StatsigDriver
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def log_event(user, event_name, value = nil, metadata = nil)
|
161
|
-
@err_boundary.capture(
|
161
|
+
@err_boundary.capture(task: lambda {
|
162
162
|
if !user.nil? && !user.instance_of?(StatsigUser)
|
163
163
|
raise Statsig::ValueError.new('Must provide a valid StatsigUser or nil')
|
164
164
|
end
|
@@ -175,7 +175,7 @@ class StatsigDriver
|
|
175
175
|
end
|
176
176
|
|
177
177
|
def shutdown
|
178
|
-
@err_boundary.capture(
|
178
|
+
@err_boundary.capture(task: lambda {
|
179
179
|
@shutdown = true
|
180
180
|
@logger.shutdown
|
181
181
|
@evaluator.shutdown
|
@@ -183,13 +183,13 @@ class StatsigDriver
|
|
183
183
|
end
|
184
184
|
|
185
185
|
def override_gate(gate_name, gate_value)
|
186
|
-
@err_boundary.capture(
|
186
|
+
@err_boundary.capture(task: lambda {
|
187
187
|
@evaluator.override_gate(gate_name, gate_value)
|
188
188
|
})
|
189
189
|
end
|
190
190
|
|
191
191
|
def override_config(config_name, config_value)
|
192
|
-
@err_boundary.capture(
|
192
|
+
@err_boundary.capture(task: lambda {
|
193
193
|
@evaluator.override_config(config_name, config_value)
|
194
194
|
})
|
195
195
|
end
|
@@ -197,11 +197,11 @@ class StatsigDriver
|
|
197
197
|
# @param [StatsigUser] user
|
198
198
|
# @return [Hash]
|
199
199
|
def get_client_initialize_response(user)
|
200
|
-
@err_boundary.capture(
|
200
|
+
@err_boundary.capture(task: lambda {
|
201
201
|
validate_user(user)
|
202
202
|
normalize_user(user)
|
203
203
|
@evaluator.get_client_initialize_response(user)
|
204
|
-
}, -> { nil })
|
204
|
+
}, recover: -> { nil })
|
205
205
|
end
|
206
206
|
|
207
207
|
def maybe_restart_background_threads
|
@@ -209,7 +209,7 @@ class StatsigDriver
|
|
209
209
|
return
|
210
210
|
end
|
211
211
|
|
212
|
-
@err_boundary.capture(
|
212
|
+
@err_boundary.capture(task: lambda {
|
213
213
|
@evaluator.maybe_restart_background_threads
|
214
214
|
@logger.maybe_restart_background_threads
|
215
215
|
})
|
@@ -302,12 +302,4 @@ class StatsigDriver
|
|
302
302
|
network_result['rule_id'],
|
303
303
|
)
|
304
304
|
end
|
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
305
|
end
|
data/lib/statsig_logger.rb
CHANGED
@@ -97,10 +97,14 @@ module Statsig
|
|
97
97
|
end
|
98
98
|
|
99
99
|
def log_diagnostics_event(diagnostics, user = nil)
|
100
|
+
return if @options.disable_diagnostics_logging
|
101
|
+
return if diagnostics.nil? || diagnostics.markers.empty?
|
102
|
+
|
100
103
|
event = StatsigEvent.new($diagnostics_event)
|
101
104
|
event.user = user
|
102
105
|
event.metadata = diagnostics.serialize
|
103
106
|
log_event(event)
|
107
|
+
diagnostics.clear_markers
|
104
108
|
end
|
105
109
|
|
106
110
|
def periodic_flush
|
data/lib/statsig_options.rb
CHANGED
@@ -19,6 +19,11 @@ class StatsigOptions
|
|
19
19
|
# default: https://statsigapi.net/v1
|
20
20
|
attr_accessor :api_url_base
|
21
21
|
|
22
|
+
# The base url used specifically to call download_config_specs.
|
23
|
+
# Takes precedence over api_url_base
|
24
|
+
sig { returns(T.any(String, NilClass)) }
|
25
|
+
attr_accessor :api_url_download_config_specs
|
26
|
+
|
22
27
|
sig { returns(T.any(Float, Integer)) }
|
23
28
|
# The interval (in seconds) to poll for changes to your Statsig configuration
|
24
29
|
# default: 10s
|
@@ -97,6 +102,7 @@ class StatsigOptions
|
|
97
102
|
params(
|
98
103
|
environment: T.any(T::Hash[String, String], NilClass),
|
99
104
|
api_url_base: String,
|
105
|
+
api_url_download_config_specs: T.any(String, NilClass),
|
100
106
|
rulesets_sync_interval: T.any(Float, Integer),
|
101
107
|
idlists_sync_interval: T.any(Float, Integer),
|
102
108
|
logging_interval_seconds: T.any(Float, Integer),
|
@@ -118,6 +124,7 @@ class StatsigOptions
|
|
118
124
|
def initialize(
|
119
125
|
environment = nil,
|
120
126
|
api_url_base = 'https://statsigapi.net/v1',
|
127
|
+
api_url_download_config_specs: nil,
|
121
128
|
rulesets_sync_interval: 10,
|
122
129
|
idlists_sync_interval: 60,
|
123
130
|
logging_interval_seconds: 60,
|
@@ -135,6 +142,7 @@ class StatsigOptions
|
|
135
142
|
post_logs_retry_backoff: nil)
|
136
143
|
@environment = environment.is_a?(Hash) ? environment : nil
|
137
144
|
@api_url_base = api_url_base
|
145
|
+
@api_url_download_config_specs = api_url_download_config_specs
|
138
146
|
@rulesets_sync_interval = rulesets_sync_interval
|
139
147
|
@idlists_sync_interval = idlists_sync_interval
|
140
148
|
@logging_interval_seconds = logging_interval_seconds
|
data/lib/uri_helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
class URIHelper
|
6
|
+
class URIBuilder
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(StatsigOptions) }
|
10
|
+
attr_accessor :options
|
11
|
+
|
12
|
+
sig { params(options: StatsigOptions).void }
|
13
|
+
def initialize(options)
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(endpoint: String).returns(String) }
|
18
|
+
def build_url(endpoint)
|
19
|
+
api = @options.api_url_base
|
20
|
+
if endpoint == 'download_config_specs' && !@options.api_url_download_config_specs.nil?
|
21
|
+
api = T.must(@options.api_url_download_config_specs)
|
22
|
+
end
|
23
|
+
unless api.end_with?('/')
|
24
|
+
api += '/'
|
25
|
+
end
|
26
|
+
"#{api}#{endpoint}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.initialize(options)
|
31
|
+
@uri_builder = URIBuilder.new(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.build_url(endpoint)
|
35
|
+
@uri_builder.build_url(endpoint)
|
36
|
+
end
|
37
|
+
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.25.
|
4
|
+
version: 1.25.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Statsig, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -295,6 +295,7 @@ files:
|
|
295
295
|
- lib/statsig_options.rb
|
296
296
|
- lib/statsig_user.rb
|
297
297
|
- lib/ua_parser.rb
|
298
|
+
- lib/uri_helper.rb
|
298
299
|
homepage: https://rubygems.org/gems/statsig
|
299
300
|
licenses:
|
300
301
|
- ISC
|