statsig 1.25.1 → 1.25.2
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 +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
|