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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 800d73e16d593bd9c6b505f9d6207c47e51d8b700344b3c653a07ca8b257cd69
4
- data.tar.gz: a3cad13252aff42d5eba3e4f9c9b478adb9ec0758633102f2ada9b3d3739052e
3
+ metadata.gz: 5371899fd2582b8e213db4bfc0c0c9252e2a7e16ff1303c94fa7e1cecc43cfda
4
+ data.tar.gz: 786baba109ff66e4c38ffc4c71995fdf5fde54910ac0d7712cdfb9b85ac584f4
5
5
  SHA512:
6
- metadata.gz: f9a1df9789f61b60bb4d015da636b20bf081804b5c1c8d1f542afc3f494538fea68b3bc84ca65d7faf513c63d8d0d72bee94f2583a932c7d54ae0c3cdff560e7
7
- data.tar.gz: a7d46bfa64e8a52c60eec3bc14c596ff9ffe41493ed68bda741d4171c75da407e7e7842ad63c76addff0c98314d95967b515801efba8f1f5486d8aabc793fc8e
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 * @backoff_multiplier, zipped, event_count, extra_headers, use_statsig_headers)
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 * @backoff_multiplier, zipped, event_count, extra_headers, use_statsig_headers)
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
- download_config_specs('config_sync')
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
- tracker = @diagnostics.track(context, 'download_config_specs', 'network_request')
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
@@ -410,7 +410,7 @@ module Statsig
410
410
  def self.get_statsig_metadata
411
411
  {
412
412
  'sdkType' => 'ruby-server',
413
- 'sdkVersion' => '2.9.1',
413
+ 'sdkVersion' => '2.9.3',
414
414
  'languageVersion' => RUBY_VERSION
415
415
  }
416
416
  end
@@ -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
- if @shutdown
464
- puts 'SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
465
- end
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
@@ -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.1
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-04-29 00:00:00.000000000 Z
11
+ date: 2026-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler