statsig 2.8.3 → 2.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a17857d17823d2f8d7f6a13d01c4f3d117e4187233d377441c4e7be7a6499ef1
4
- data.tar.gz: 34bb6a509d1fca2ab2973edf7ce0f76814440c5197649a68af8c573130ef13d5
3
+ metadata.gz: fbf7ba0eaa7ee393b1c4060244eeef4e2dfc83d8554532511409825a22bee21b
4
+ data.tar.gz: f3509f7c05fa8d90c8b38b99b2366eded563e4cbeca91f13b01924ea3c66d35f
5
5
  SHA512:
6
- metadata.gz: eef23c96fefa5aff93eec2ce8562a45ccb59434bfb68d264f39b50df987689c402286895103295b701ba6f71db970f43bd8832f608a2da0622a5bde1717d9521
7
- data.tar.gz: 7df3f01db686cf387e30b7d45be967be4d9376340d64506359c0df6a0a9b169f45ef310a7b727b34f87cb9c4ddb97f7ba0a80daf7e79d8102f256f83f32369cd
6
+ metadata.gz: 4fc3b066283642cee6990b2ca8a12f21a3355adec4c09d2cc25fabeed97ace70ebd23f565d56d053c7d77946dbd76cc565dccb94ab7d17868ba6904f84d433e1
7
+ data.tar.gz: 2d8064a954dab46521ac9034cca3a8d920266d408d9337149d1fce92973338220f82b47781c35292a8c963868e2f5d866b08df251cb1c63701ee12cc5b798b12
data/lib/network.rb CHANGED
@@ -28,34 +28,19 @@ module Statsig
28
28
  @post_logs_retry_backoff = options.post_logs_retry_backoff
29
29
  @post_logs_retry_limit = options.post_logs_retry_limit
30
30
  @session_id = SecureRandom.uuid
31
- @connection_pool = ConnectionPool.new(size: 3) do
32
- meta = Statsig.get_statsig_metadata
33
- client = HTTP.use(:auto_inflate).headers(
34
- {
35
- 'STATSIG-API-KEY' => @server_secret,
36
- 'STATSIG-SERVER-SESSION-ID' => @session_id,
37
- 'Content-Type' => 'application/json; charset=UTF-8',
38
- 'STATSIG-SDK-TYPE' => meta['sdkType'],
39
- 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
40
- 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
41
- 'Accept-Encoding' => 'gzip'
42
- }
43
- ).accept(:json)
44
-
45
- if @timeout
46
- client = client.timeout(@timeout)
47
- end
48
-
49
- client
50
- end
31
+ @connection_pools = {}
32
+ @connection_pools_mutex = Mutex.new
51
33
  end
52
34
 
53
- def download_config_specs(since_time)
35
+ def download_config_specs(since_time, context)
54
36
  url = @options.download_config_specs_url
55
37
  dcs_url = "#{url}#{@server_secret}.json"
56
38
  if since_time.positive?
57
39
  dcs_url += "?sinceTime=#{since_time}"
58
40
  end
41
+ if context == 'initialize'
42
+ return get(dcs_url, @options.initialize_retry_limit)
43
+ end
59
44
  get(dcs_url)
60
45
  end
61
46
 
@@ -66,10 +51,7 @@ module Statsig
66
51
  gzip = Zlib::GzipWriter.new(StringIO.new)
67
52
  gzip << json_body
68
53
 
69
- response, e = post(url, gzip.close.string, @post_logs_retry_limit, 1, true, event_count)
70
-
71
- # Consume response body to ensure connection can be closed.
72
- response&.flush
54
+ _response, e = post(url, gzip.close.string, @post_logs_retry_limit, 1, true, event_count)
73
55
 
74
56
  unless e == nil
75
57
  message = "Failed to log #{event_count} events after #{@post_logs_retry_limit} retries"
@@ -81,11 +63,18 @@ module Statsig
81
63
 
82
64
  end
83
65
 
84
- def get_id_lists
66
+ def get_id_lists(context)
85
67
  url = @options.get_id_lists_url
68
+ if context == 'initialize'
69
+ return post(url, JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }), @options.initialize_retry_limit)
70
+ end
86
71
  post(url, JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
87
72
  end
88
73
 
74
+ def download_id_list(url, start_byte = 0)
75
+ request(:GET, url, nil, 0, 1, false, 0, { 'Range' => "bytes=#{start_byte}-" }, false)
76
+ end
77
+
89
78
  def get(url, retries = 0, backoff = 1)
90
79
  request(:GET, url, nil, retries, backoff)
91
80
  end
@@ -94,7 +83,21 @@ module Statsig
94
83
  request(:POST, url, body, retries, backoff, zipped, event_count)
95
84
  end
96
85
 
97
- def request(method, url, body, retries = 0, backoff = 1, zipped = false, event_count = 0)
86
+ def shutdown
87
+ pools = @connection_pools_mutex.synchronize do
88
+ pools = @connection_pools.values
89
+ @connection_pools = {}
90
+ pools
91
+ end
92
+
93
+ pools.each do |pool|
94
+ pool.shutdown do |conn|
95
+ conn.close if conn.respond_to?(:close)
96
+ end
97
+ end
98
+ end
99
+
100
+ def request(method, url, body, retries = 0, backoff = 1, zipped = false, event_count = 0, extra_headers = {}, use_statsig_headers = true)
98
101
  if @local_mode
99
102
  return nil, nil
100
103
  end
@@ -108,18 +111,25 @@ module Statsig
108
111
  end
109
112
 
110
113
  begin
111
- res = @connection_pool.with do |conn|
112
- request = conn.headers(
113
- 'STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s,
114
- 'CONTENT-ENCODING' => zipped ? 'gzip' : nil,
115
- 'STATSIG-EVENT-COUNT' => event_count == 0 ? nil : event_count.to_s
116
- )
117
-
118
- case method
119
- when :GET
120
- request.get(url)
121
- when :POST
122
- request.post(url, body: body)
114
+ pool = connection_pool_for(url)
115
+ res = pool.with do |conn|
116
+ begin
117
+ request_headers = build_request_headers(zipped, event_count, extra_headers, use_statsig_headers)
118
+ request = conn.headers(request_headers)
119
+
120
+ response = case method
121
+ when :GET
122
+ request.get(url)
123
+ when :POST
124
+ request.post(url, body: body)
125
+ end
126
+
127
+ # Fully drain the response before the client returns to the pool.
128
+ response.body.to_s
129
+ response
130
+ rescue StandardError
131
+ pool.discard_current_connection(&:close)
132
+ raise
123
133
  end
124
134
  end
125
135
  rescue StandardError => e
@@ -127,7 +137,7 @@ module Statsig
127
137
  return nil, e unless retries.positive?
128
138
 
129
139
  sleep backoff_adjusted
130
- return request(method, url, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count)
140
+ return request(method, url, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count, extra_headers, use_statsig_headers)
131
141
  end
132
142
  return res, nil if res.status.success?
133
143
 
@@ -138,7 +148,50 @@ module Statsig
138
148
 
139
149
  ## status code retry
140
150
  sleep backoff_adjusted
141
- request(method, url, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count)
151
+ request(method, url, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count, extra_headers, use_statsig_headers)
152
+ end
153
+
154
+ private
155
+
156
+ def connection_pool_for(url)
157
+ origin = HTTP::URI.parse(url).origin
158
+ @connection_pools_mutex.synchronize do
159
+ @connection_pools[origin] ||= ConnectionPool.new(size: 3) do
160
+ build_persistent_client(origin)
161
+ end
162
+ end
163
+ end
164
+
165
+ def build_persistent_client(origin)
166
+ client = HTTP.use(:auto_inflate)
167
+ client = client.timeout(@timeout) if @timeout
168
+ client.persistent(origin)
169
+ end
170
+
171
+ def build_request_headers(zipped, event_count, extra_headers, use_statsig_headers)
172
+ headers = {}
173
+
174
+ if use_statsig_headers
175
+ meta = Statsig.get_statsig_metadata
176
+ headers.merge!(
177
+ 'STATSIG-API-KEY' => @server_secret,
178
+ 'STATSIG-SERVER-SESSION-ID' => @session_id,
179
+ 'Content-Type' => 'application/json; charset=UTF-8',
180
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
181
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
182
+ 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
183
+ 'Accept' => 'application/json',
184
+ 'Accept-Encoding' => 'gzip',
185
+ 'STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s
186
+ )
187
+ end
188
+
189
+ headers['CONTENT-ENCODING'] = 'gzip' if zipped
190
+ headers['STATSIG-EVENT-COUNT'] = event_count.to_s unless event_count == 0
191
+
192
+ headers.merge!(extra_headers)
193
+ headers.delete_if { |_key, value| value.nil? }
194
+ headers
142
195
  end
143
196
  end
144
197
  end
data/lib/spec_store.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  require 'concurrent-ruby'
2
- require 'net/http'
3
- require 'uri'
4
2
  require_relative 'api_config'
5
3
  require_relative 'evaluation_details'
6
4
  require_relative 'hash_utils'
@@ -291,7 +289,7 @@ module Statsig
291
289
  error = nil
292
290
  failure_details = nil
293
291
  begin
294
- response, e = @network.download_config_specs(@last_config_sync_time)
292
+ response, e = @network.download_config_specs(@last_config_sync_time, context)
295
293
  code = response&.status.to_i
296
294
  if e.is_a? NetworkError
297
295
  code = e.http_code
@@ -302,14 +300,15 @@ module Statsig
302
300
  if e.nil?
303
301
  unless response.nil?
304
302
  tracker = @diagnostics.track(context, 'download_config_specs', 'process')
305
- failure_details = process_specs(response.body.to_s)
303
+ body = response.body.to_s
304
+ failure_details = process_specs(body)
306
305
  if failure_details.nil?
307
306
  @init_reason = EvaluationReason::NETWORK
308
307
  end
309
308
  tracker.end(success: @init_reason == EvaluationReason::NETWORK)
310
309
 
311
310
  unless response.body.nil? or @rules_updated_callback.nil?
312
- @rules_updated_callback.call(response.body.to_s,
311
+ @rules_updated_callback.call(body,
313
312
  @last_config_sync_time)
314
313
  end
315
314
  end
@@ -401,7 +400,7 @@ module Statsig
401
400
 
402
401
  def get_id_lists_from_network(context)
403
402
  tracker = @diagnostics.track(context, 'get_id_list_sources', 'network_request')
404
- response, e = @network.get_id_lists
403
+ response, e = @network.get_id_lists(context)
405
404
  code = response&.status.to_i
406
405
  if e.is_a? NetworkError
407
406
  code = e.http_code
@@ -413,9 +412,10 @@ module Statsig
413
412
  end
414
413
 
415
414
  begin
416
- server_id_lists = JSON.parse(response)
415
+ body = response.body.to_s
416
+ server_id_lists = JSON.parse(body)
417
417
  process_id_lists(server_id_lists, context)
418
- save_id_lists_to_adapter(response.body.to_s)
418
+ save_id_lists_to_adapter(body)
419
419
  rescue StandardError
420
420
  # Ignored, will try again
421
421
  end
@@ -509,14 +509,18 @@ module Statsig
509
509
 
510
510
  def download_single_id_list(list, context)
511
511
  nil unless list.is_a? IDList
512
- http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
513
512
  tracker = @diagnostics.track(context, 'get_id_list', 'network_request', { url: list.url })
514
513
  begin
515
- res = http.get(list.url)
516
- tracker.end(statusCode: res.status.code, success: res.status.success?)
517
- nil unless res.status.success?
514
+ res, e = @network.download_id_list(list.url, list&.size || 0)
515
+ code = res&.status&.to_i
516
+ if e.is_a? NetworkError
517
+ code = e.http_code
518
+ end
519
+ success = e.nil? && !res.nil?
520
+ tracker.end(statusCode: code, success: success)
521
+ return nil unless success
518
522
  content_length = Integer(res['content-length'])
519
- nil if content_length.nil? || content_length <= 0
523
+ return nil if content_length.nil? || content_length <= 0
520
524
  content = res.body.to_s
521
525
  success = process_single_id_list(list, context, content, content_length)
522
526
  save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
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.8.3',
413
+ 'sdkVersion' => '2.9.0',
414
414
  'languageVersion' => RUBY_VERSION
415
415
  }
416
416
  end
@@ -291,8 +291,12 @@ class StatsigDriver
291
291
  def shutdown
292
292
  @err_boundary.capture(caller: __method__) do
293
293
  @shutdown = true
294
- @logger.shutdown
295
- @evaluator.shutdown
294
+ begin
295
+ @logger.shutdown
296
+ @evaluator.shutdown
297
+ ensure
298
+ @net.shutdown
299
+ end
296
300
  end
297
301
  end
298
302
 
@@ -92,6 +92,10 @@ class StatsigOptions
92
92
  # default: false
93
93
  attr_accessor :disable_evaluation_memoization
94
94
 
95
+ # Number of times to retry fetching rulesets and id lists on initialization.
96
+ # default: 0
97
+ attr_accessor :initialize_retry_limit
98
+
95
99
  def initialize(
96
100
  environment = nil,
97
101
  download_config_specs_url: nil,
@@ -115,7 +119,8 @@ class StatsigOptions
115
119
  post_logs_retry_limit: 3,
116
120
  post_logs_retry_backoff: nil,
117
121
  user_persistent_storage: nil,
118
- disable_evaluation_memoization: false
122
+ disable_evaluation_memoization: false,
123
+ initialize_retry_limit: 0
119
124
  )
120
125
  @environment = environment.is_a?(Hash) ? environment : nil
121
126
 
@@ -146,5 +151,6 @@ class StatsigOptions
146
151
  @post_logs_retry_backoff = post_logs_retry_backoff
147
152
  @user_persistent_storage = user_persistent_storage
148
153
  @disable_evaluation_memoization = disable_evaluation_memoization
154
+ @initialize_retry_limit = initialize_retry_limit
149
155
  end
150
156
  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.8.3
4
+ version: 2.9.0
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-01-26 00:00:00.000000000 Z
11
+ date: 2026-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler