statsig 2.9.1 → 2.9.3
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/network.rb +22 -5
- data/lib/spec_store.rb +72 -40
- data/lib/statsig.rb +1 -1
- data/lib/statsig_driver.rb +6 -3
- data/lib/statsig_options.rb +9 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5371899fd2582b8e213db4bfc0c0c9252e2a7e16ff1303c94fa7e1cecc43cfda
|
|
4
|
+
data.tar.gz: 786baba109ff66e4c38ffc4c71995fdf5fde54910ac0d7712cdfb9b85ac584f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4dc2ad08e71c883d41ecfa012f9e592c6ece9e18e8e14b9f0df9c5ef0732140dddf7cd0f6b88e27516efdbfa89e0d7309e2763fb634b73da0de082eb78b84f3
|
|
7
|
+
data.tar.gz: 9fc9b841b4d3f6020b9ea71a87203155a5eef84641ac1f7ff52a1e8913ef1735e3f49ef3730575d67050b1c00b7e61d0953a00b4cfc001af8e5b31277499f6d1
|
data/lib/network.rb
CHANGED
|
@@ -7,6 +7,7 @@ require 'zlib'
|
|
|
7
7
|
RETRY_CODES = [408, 500, 502, 503, 504, 522, 524, 599].freeze
|
|
8
8
|
|
|
9
9
|
module Statsig
|
|
10
|
+
STATSIG_CDN_DCS_BASE = 'https://api.statsigcdn.com/v2'.freeze
|
|
10
11
|
class NetworkError < StandardError
|
|
11
12
|
attr_reader :http_code
|
|
12
13
|
|
|
@@ -18,6 +19,14 @@ module Statsig
|
|
|
18
19
|
|
|
19
20
|
class Network
|
|
20
21
|
|
|
22
|
+
# Background config-spec sync retries transient failures with a short, bounded
|
|
23
|
+
# exponential backoff, matching statsig-server-core's background sync strategy
|
|
24
|
+
# (3 retries; 2^n * 100ms => 200ms, 400ms, 800ms). These are fixed rather than
|
|
25
|
+
# user-configurable, mirroring server-core where the values are hardcoded.
|
|
26
|
+
CONFIG_SYNC_RETRY_LIMIT = 3
|
|
27
|
+
CONFIG_SYNC_RETRY_BACKOFF_SECONDS = 0.2
|
|
28
|
+
CONFIG_SYNC_RETRY_BACKOFF_MULTIPLIER = 2
|
|
29
|
+
|
|
21
30
|
def initialize(server_secret, options, backoff_mult = 10)
|
|
22
31
|
super()
|
|
23
32
|
@options = options
|
|
@@ -41,6 +50,14 @@ module Statsig
|
|
|
41
50
|
if context == 'initialize'
|
|
42
51
|
return get(dcs_url, @options.initialize_retry_limit)
|
|
43
52
|
end
|
|
53
|
+
get(dcs_url, CONFIG_SYNC_RETRY_LIMIT, CONFIG_SYNC_RETRY_BACKOFF_SECONDS, CONFIG_SYNC_RETRY_BACKOFF_MULTIPLIER)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def download_config_specs_fallback(since_time, context)
|
|
57
|
+
dcs_url = "#{STATSIG_CDN_DCS_BASE}/download_config_specs/#{@server_secret}.json"
|
|
58
|
+
if since_time.positive?
|
|
59
|
+
dcs_url += "?sinceTime=#{since_time}"
|
|
60
|
+
end
|
|
44
61
|
get(dcs_url)
|
|
45
62
|
end
|
|
46
63
|
|
|
@@ -75,8 +92,8 @@ module Statsig
|
|
|
75
92
|
request(:GET, url, nil, 0, 1, false, 0, { 'Range' => "bytes=#{start_byte}-" }, false)
|
|
76
93
|
end
|
|
77
94
|
|
|
78
|
-
def get(url, retries = 0, backoff = 1)
|
|
79
|
-
request(:GET, url, nil, retries, backoff)
|
|
95
|
+
def get(url, retries = 0, backoff = 1, backoff_multiplier = @backoff_multiplier)
|
|
96
|
+
request(:GET, url, nil, retries, backoff, false, 0, {}, true, backoff_multiplier)
|
|
80
97
|
end
|
|
81
98
|
|
|
82
99
|
def post(url, body, retries = 0, backoff = 1, zipped = false, event_count = 0)
|
|
@@ -97,7 +114,7 @@ module Statsig
|
|
|
97
114
|
end
|
|
98
115
|
end
|
|
99
116
|
|
|
100
|
-
def request(method, url, body, retries = 0, backoff = 1, zipped = false, event_count = 0, extra_headers = {}, use_statsig_headers = true)
|
|
117
|
+
def request(method, url, body, retries = 0, backoff = 1, zipped = false, event_count = 0, extra_headers = {}, use_statsig_headers = true, backoff_multiplier = @backoff_multiplier)
|
|
101
118
|
if @local_mode
|
|
102
119
|
return nil, nil
|
|
103
120
|
end
|
|
@@ -137,7 +154,7 @@ module Statsig
|
|
|
137
154
|
return nil, e unless retries.positive?
|
|
138
155
|
|
|
139
156
|
sleep backoff_adjusted
|
|
140
|
-
return request(method, url, body, retries - 1, backoff *
|
|
157
|
+
return request(method, url, body, retries - 1, backoff * backoff_multiplier, zipped, event_count, extra_headers, use_statsig_headers, backoff_multiplier)
|
|
141
158
|
end
|
|
142
159
|
return res, nil if res.status.success?
|
|
143
160
|
|
|
@@ -148,7 +165,7 @@ module Statsig
|
|
|
148
165
|
|
|
149
166
|
## status code retry
|
|
150
167
|
sleep backoff_adjusted
|
|
151
|
-
request(method, url, body, retries - 1, backoff *
|
|
168
|
+
request(method, url, body, retries - 1, backoff * backoff_multiplier, zipped, event_count, extra_headers, use_statsig_headers, backoff_multiplier)
|
|
152
169
|
end
|
|
153
170
|
|
|
154
171
|
private
|
data/lib/spec_store.rb
CHANGED
|
@@ -6,6 +6,8 @@ require_relative 'id_list'
|
|
|
6
6
|
|
|
7
7
|
module Statsig
|
|
8
8
|
class SpecStore
|
|
9
|
+
STATSIG_NETWORK_FALLBACK_THRESHOLD = 5
|
|
10
|
+
|
|
9
11
|
attr_accessor :last_config_sync_time
|
|
10
12
|
attr_accessor :initial_config_sync_time
|
|
11
13
|
attr_accessor :init_reason
|
|
@@ -48,6 +50,7 @@ module Statsig
|
|
|
48
50
|
@secret_key = secret_key
|
|
49
51
|
@unsupported_configs = Set.new
|
|
50
52
|
@sdk_configs = sdk_config
|
|
53
|
+
@sync_failure_count = 0
|
|
51
54
|
|
|
52
55
|
startTime = (Time.now.to_f * 1000).to_i
|
|
53
56
|
|
|
@@ -84,6 +87,9 @@ module Statsig
|
|
|
84
87
|
|
|
85
88
|
if @init_reason == EvaluationReason::UNINITIALIZED
|
|
86
89
|
failure_details = download_config_specs('initialize')
|
|
90
|
+
if !failure_details.nil? && @options.fallback_to_statsig_api && using_proxy_for_dcs?
|
|
91
|
+
failure_details = download_config_specs_fallback('initialize')
|
|
92
|
+
end
|
|
87
93
|
end
|
|
88
94
|
|
|
89
95
|
@initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
|
|
@@ -205,10 +211,20 @@ module Statsig
|
|
|
205
211
|
|
|
206
212
|
def sync_config_specs
|
|
207
213
|
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_V2_KEY)
|
|
208
|
-
load_config_specs_from_storage_adapter('config_sync')
|
|
214
|
+
failure_details = load_config_specs_from_storage_adapter('config_sync')
|
|
215
|
+
else
|
|
216
|
+
failure_details = download_config_specs('config_sync')
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
if failure_details.nil?
|
|
220
|
+
@sync_failure_count = 0
|
|
209
221
|
else
|
|
210
|
-
|
|
222
|
+
@sync_failure_count += 1
|
|
223
|
+
if @options.fallback_to_statsig_api && using_proxy_for_dcs? && (@sync_failure_count % STATSIG_NETWORK_FALLBACK_THRESHOLD == 0)
|
|
224
|
+
download_config_specs_fallback('config_sync')
|
|
225
|
+
end
|
|
211
226
|
end
|
|
227
|
+
|
|
212
228
|
@logger.log_diagnostics_event(@diagnostics, 'config_sync')
|
|
213
229
|
end
|
|
214
230
|
|
|
@@ -223,6 +239,58 @@ module Statsig
|
|
|
223
239
|
|
|
224
240
|
private
|
|
225
241
|
|
|
242
|
+
def using_proxy_for_dcs?
|
|
243
|
+
!@options.download_config_specs_url.start_with?(STATSIG_CDN_DCS_BASE)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def download_config_specs_fallback(context)
|
|
247
|
+
response, e = @network.download_config_specs_fallback(@last_config_sync_time, context)
|
|
248
|
+
handle_config_specs_response(context, 'download_config_specs_fallback', 'CONFIG_SPECS_FALLBACK', response, e)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def handle_config_specs_response(context, diagnostic_key, error_prefix, response, e)
|
|
252
|
+
tracker = @diagnostics.track(context, diagnostic_key, 'network_request')
|
|
253
|
+
|
|
254
|
+
error = nil
|
|
255
|
+
failure_details = nil
|
|
256
|
+
begin
|
|
257
|
+
code = response&.status.to_i
|
|
258
|
+
if e.is_a? NetworkError
|
|
259
|
+
code = e.http_code
|
|
260
|
+
failure_details = {statusCode: code, exception: e, reason: "#{error_prefix}_NETWORK_ERROR"}
|
|
261
|
+
elsif !e.nil?
|
|
262
|
+
# Transport-level errors (ECONNREFUSED, timeout, DNS, SSL) are caught and returned as values
|
|
263
|
+
# by network.rb. Without this, failure_details stays nil and the caller treats it as success.
|
|
264
|
+
failure_details = {exception: e, reason: "#{error_prefix}_CONNECTION_ERROR"}
|
|
265
|
+
end
|
|
266
|
+
tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
|
267
|
+
|
|
268
|
+
if e.nil?
|
|
269
|
+
unless response.nil?
|
|
270
|
+
tracker = @diagnostics.track(context, diagnostic_key, 'process')
|
|
271
|
+
body = response.body.to_s
|
|
272
|
+
failure_details = process_specs(body)
|
|
273
|
+
if failure_details.nil?
|
|
274
|
+
@init_reason = EvaluationReason::NETWORK
|
|
275
|
+
end
|
|
276
|
+
tracker.end(success: @init_reason == EvaluationReason::NETWORK)
|
|
277
|
+
|
|
278
|
+
unless response.body.nil? or @rules_updated_callback.nil?
|
|
279
|
+
@rules_updated_callback.call(body, @last_config_sync_time)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
error = e
|
|
284
|
+
end
|
|
285
|
+
rescue StandardError => e
|
|
286
|
+
failure_details = {exception: e, reason: "INTERNAL_ERROR"}
|
|
287
|
+
error = e
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
@error_callback.call(error) unless error.nil? or @error_callback.nil?
|
|
291
|
+
return failure_details
|
|
292
|
+
end
|
|
293
|
+
|
|
226
294
|
def load_config_specs_from_storage_adapter(context)
|
|
227
295
|
tracker = @diagnostics.track(context, 'data_store_config_specs', 'fetch')
|
|
228
296
|
cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_V2_KEY)
|
|
@@ -284,44 +352,8 @@ module Statsig
|
|
|
284
352
|
end
|
|
285
353
|
|
|
286
354
|
def download_config_specs(context)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
error = nil
|
|
290
|
-
failure_details = nil
|
|
291
|
-
begin
|
|
292
|
-
response, e = @network.download_config_specs(@last_config_sync_time, context)
|
|
293
|
-
code = response&.status.to_i
|
|
294
|
-
if e.is_a? NetworkError
|
|
295
|
-
code = e.http_code
|
|
296
|
-
failure_details = {statusCode: code, exception: e, reason: "CONFIG_SPECS_NETWORK_ERROR"}
|
|
297
|
-
end
|
|
298
|
-
tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
|
299
|
-
|
|
300
|
-
if e.nil?
|
|
301
|
-
unless response.nil?
|
|
302
|
-
tracker = @diagnostics.track(context, 'download_config_specs', 'process')
|
|
303
|
-
body = response.body.to_s
|
|
304
|
-
failure_details = process_specs(body)
|
|
305
|
-
if failure_details.nil?
|
|
306
|
-
@init_reason = EvaluationReason::NETWORK
|
|
307
|
-
end
|
|
308
|
-
tracker.end(success: @init_reason == EvaluationReason::NETWORK)
|
|
309
|
-
|
|
310
|
-
unless response.body.nil? or @rules_updated_callback.nil?
|
|
311
|
-
@rules_updated_callback.call(body,
|
|
312
|
-
@last_config_sync_time)
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
else
|
|
316
|
-
error = e
|
|
317
|
-
end
|
|
318
|
-
rescue StandardError => e
|
|
319
|
-
failure_details = {exception: e, reason: "INTERNAL_ERROR"}
|
|
320
|
-
error = e
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
@error_callback.call(error) unless error.nil? or @error_callback.nil?
|
|
324
|
-
return failure_details
|
|
355
|
+
response, e = @network.download_config_specs(@last_config_sync_time, context)
|
|
356
|
+
handle_config_specs_response(context, 'download_config_specs', 'CONFIG_SPECS', response, e)
|
|
325
357
|
end
|
|
326
358
|
|
|
327
359
|
def process_specs(specs_string, from_adapter: false)
|
data/lib/statsig.rb
CHANGED
data/lib/statsig_driver.rb
CHANGED
|
@@ -34,6 +34,7 @@ class StatsigDriver
|
|
|
34
34
|
tracker = @diagnostics.track('initialize', 'overall')
|
|
35
35
|
@options = options || StatsigOptions.new
|
|
36
36
|
@shutdown = false
|
|
37
|
+
@shutdown_message_logged = false
|
|
37
38
|
@secret_key = secret_key
|
|
38
39
|
@net = Statsig::Network.new(secret_key, @options)
|
|
39
40
|
@logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary, @sdk_configs)
|
|
@@ -460,8 +461,10 @@ class StatsigDriver
|
|
|
460
461
|
end
|
|
461
462
|
|
|
462
463
|
def check_shutdown
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
return unless @shutdown
|
|
465
|
+
return if @shutdown_message_logged
|
|
466
|
+
|
|
467
|
+
@shutdown_message_logged = true
|
|
468
|
+
puts '[Statsig]: SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
|
|
466
469
|
end
|
|
467
470
|
end
|
data/lib/statsig_options.rb
CHANGED
|
@@ -96,6 +96,12 @@ class StatsigOptions
|
|
|
96
96
|
# default: 0
|
|
97
97
|
attr_accessor :initialize_retry_limit
|
|
98
98
|
|
|
99
|
+
# When true, if the configured download_config_specs_url fails, the SDK will
|
|
100
|
+
# automatically fall back to the Statsig CDN to fetch config specs.
|
|
101
|
+
# Only effective when a custom download_config_specs_url is configured.
|
|
102
|
+
# default: false
|
|
103
|
+
attr_accessor :fallback_to_statsig_api
|
|
104
|
+
|
|
99
105
|
def initialize(
|
|
100
106
|
environment = nil,
|
|
101
107
|
download_config_specs_url: nil,
|
|
@@ -120,7 +126,8 @@ class StatsigOptions
|
|
|
120
126
|
post_logs_retry_backoff: nil,
|
|
121
127
|
user_persistent_storage: nil,
|
|
122
128
|
disable_evaluation_memoization: false,
|
|
123
|
-
initialize_retry_limit: 0
|
|
129
|
+
initialize_retry_limit: 0,
|
|
130
|
+
fallback_to_statsig_api: false
|
|
124
131
|
)
|
|
125
132
|
@environment = environment.is_a?(Hash) ? environment : nil
|
|
126
133
|
|
|
@@ -152,5 +159,6 @@ class StatsigOptions
|
|
|
152
159
|
@user_persistent_storage = user_persistent_storage
|
|
153
160
|
@disable_evaluation_memoization = disable_evaluation_memoization
|
|
154
161
|
@initialize_retry_limit = initialize_retry_limit
|
|
162
|
+
@fallback_to_statsig_api = fallback_to_statsig_api
|
|
155
163
|
end
|
|
156
164
|
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: 2.9.
|
|
4
|
+
version: 2.9.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Statsig, Inc
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|