statsig 1.21.0 → 1.23.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: e976fe13de26ad1653e42b8a5a710210b51234cb96e6eaed6cbd350097f2370f
4
- data.tar.gz: d0a3e427187fd4ae7cd0ff0b0c0e3c2bb88c3cf5dd7ed14a272f5dae89b18ea1
3
+ metadata.gz: 89aaef291fd8cbd82af8bcf380894ea554588988b2de66507482420b475c8367
4
+ data.tar.gz: 3f535a1ce2a9af6e811c77df334b3b58a3c2503857f74bbf5b280764cfb041f1
5
5
  SHA512:
6
- metadata.gz: 2eeaf918074df7b66ffd9746d7f1717efa1912be27e86698c44bd57869f2ca023590d5db9779bbf0b3312a26735781b51afc5fd7a5e726f13fc5af00f7395fec
7
- data.tar.gz: '08d264d18cd2c772f05886782999e8c5703cf92d006a3395f57af84795b4cd309b8a7efe4d98b9e9d62378f584eb3735f3c299f859f0a46e660013e543780dca'
6
+ metadata.gz: 16074e02fd22c2d0bc64e8adbd4b2551d46509e60f161b9ff5009ea28e13a995f13e0abd89c48c1c737d80dad1a869b8f15de8ac2b896a47ad6da54cdadbdd70
7
+ data.tar.gz: ac6e8241c8132844a0ee5e8858578ab3475cb227e48847fb840d4ef15219828f58c38f24d2c2e5e2bfe26714bda07f8c1068f9a15af675d4320e4a90c6c2a7c6
@@ -2,6 +2,9 @@
2
2
  module Statsig
3
3
  module Interfaces
4
4
  class IDataStore
5
+ CONFIG_SPECS_KEY = "statsig.cache"
6
+ ID_LISTS_KEY = "statsig.id_lists"
7
+
5
8
  def init
6
9
  end
7
10
 
@@ -14,6 +17,14 @@ module Statsig
14
17
 
15
18
  def shutdown
16
19
  end
20
+
21
+ ##
22
+ # Determines whether the SDK should poll for updates from
23
+ # the data adapter (instead of Statsig network) for the given key
24
+ #
25
+ # @param key Key of stored item to poll from data adapter
26
+ def should_be_used_for_querying_updates(key)
27
+ end
17
28
  end
18
29
  end
19
30
  end
data/lib/network.rb CHANGED
@@ -20,16 +20,18 @@ module Statsig
20
20
  class Network
21
21
  extend T::Sig
22
22
 
23
- sig { params(server_secret: String, api: String, local_mode: T::Boolean, backoff_mult: Integer).void }
23
+ sig { params(server_secret: String, options: StatsigOptions, backoff_mult: Integer).void }
24
24
 
25
- def initialize(server_secret, api, local_mode, backoff_mult = 10)
25
+ def initialize(server_secret, options, backoff_mult = 10)
26
26
  super()
27
+ api = options.api_url_base
27
28
  unless api.end_with?('/')
28
29
  api += '/'
29
30
  end
30
31
  @server_secret = server_secret
31
32
  @api = api
32
- @local_mode = local_mode
33
+ @local_mode = options.local_mode
34
+ @timeout = options.network_timeout
33
35
  @backoff_multiplier = backoff_mult
34
36
  @session_id = SecureRandom.uuid
35
37
  end
@@ -52,6 +54,9 @@ module Statsig
52
54
  "STATSIG-SDK-TYPE" => meta['sdkType'],
53
55
  "STATSIG-SDK-VERSION" => meta['sdkVersion'],
54
56
  }).accept(:json)
57
+ if @timeout
58
+ http = http.timeout(@timeout)
59
+ end
55
60
  begin
56
61
  res = http.post(@api + endpoint, body: body)
57
62
  rescue StandardError => e
data/lib/spec_store.rb CHANGED
@@ -8,8 +8,6 @@ require 'concurrent-ruby'
8
8
  module Statsig
9
9
  class SpecStore
10
10
 
11
- CONFIG_SPECS_KEY = "statsig.cache"
12
-
13
11
  attr_accessor :last_config_sync_time
14
12
  attr_accessor :initial_config_sync_time
15
13
  attr_accessor :init_reason
@@ -44,7 +42,7 @@ module Statsig
44
42
  puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
45
43
  else
46
44
  init_diagnostics&.mark("bootstrap", "start", "load")
47
- if process(options.bootstrap_values)
45
+ if process_specs(options.bootstrap_values)
48
46
  @init_reason = EvaluationReason::BOOTSTRAP
49
47
  end
50
48
  init_diagnostics&.mark("bootstrap", "end", "load", @init_reason == EvaluationReason::BOOTSTRAP)
@@ -57,7 +55,7 @@ module Statsig
57
55
  unless @options.data_store.nil?
58
56
  init_diagnostics&.mark("data_store", "start", "load")
59
57
  @options.data_store.init
60
- load_from_storage_adapter
58
+ load_config_specs_from_storage_adapter(init_diagnostics: init_diagnostics)
61
59
  init_diagnostics&.mark("data_store", "end", "load", @init_reason == EvaluationReason::DATA_ADAPTER)
62
60
  end
63
61
 
@@ -66,7 +64,11 @@ module Statsig
66
64
  end
67
65
 
68
66
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
69
- get_id_lists(init_diagnostics)
67
+ if !@options.data_store.nil?
68
+ get_id_lists_from_adapter(init_diagnostics)
69
+ else
70
+ get_id_lists_from_network(init_diagnostics)
71
+ end
70
72
 
71
73
  @config_sync_thread = sync_config_specs
72
74
  @id_lists_sync_thread = sync_id_lists
@@ -132,27 +134,38 @@ module Statsig
132
134
 
133
135
  private
134
136
 
135
- def load_from_storage_adapter
136
- cached_values = @options.data_store.get(CONFIG_SPECS_KEY)
137
- if cached_values.nil?
138
- return
139
- end
140
- process(cached_values, true)
137
+ def load_config_specs_from_storage_adapter(init_diagnostics: nil)
138
+ init_diagnostics&.mark("download_config_specs", "start", "fetch_from_adapter")
139
+ cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
140
+ init_diagnostics&.mark("download_config_specs", "end", "fetch_from_adapter", true)
141
+ return if cached_values.nil?
142
+
143
+ init_diagnostics&.mark("download_config_specs", "start", "process")
144
+ process_specs(cached_values, from_adapter: true)
141
145
  @init_reason = EvaluationReason::DATA_ADAPTER
146
+ init_diagnostics&.mark("download_config_specs", "end", "process", @init_reason)
147
+ rescue StandardError
148
+ # Fallback to network
149
+ init_diagnostics&.mark("download_config_specs", "end", "fetch_from_adapter", false)
150
+ download_config_specs(init_diagnostics)
142
151
  end
143
152
 
144
- def save_to_storage_adapter(specs_string)
153
+ def save_config_specs_to_storage_adapter(specs_string)
145
154
  if @options.data_store.nil?
146
155
  return
147
156
  end
148
- @options.data_store.set(CONFIG_SPECS_KEY, specs_string)
157
+ @options.data_store.set(Interfaces::IDataStore::CONFIG_SPECS_KEY, specs_string)
149
158
  end
150
159
 
151
160
  def sync_config_specs
152
161
  Thread.new do
153
162
  loop do
154
163
  sleep @options.rulesets_sync_interval
155
- download_config_specs
164
+ if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
165
+ load_config_specs_from_storage_adapter
166
+ else
167
+ download_config_specs
168
+ end
156
169
  end
157
170
  end
158
171
  end
@@ -161,7 +174,11 @@ module Statsig
161
174
  Thread.new do
162
175
  loop do
163
176
  sleep @id_lists_sync_interval
164
- get_id_lists
177
+ if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
178
+ get_id_lists_from_adapter
179
+ else
180
+ get_id_lists_from_network
181
+ end
165
182
  end
166
183
  end
167
184
  end
@@ -182,7 +199,7 @@ module Statsig
182
199
  unless response.nil?
183
200
  init_diagnostics&.mark("download_config_specs", "start", "process")
184
201
 
185
- if process(response.body)
202
+ if process_specs(response.body)
186
203
  @init_reason = EvaluationReason::NETWORK
187
204
  @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
188
205
  end
@@ -201,7 +218,7 @@ module Statsig
201
218
  @error_callback.call(error) unless error.nil? or @error_callback.nil?
202
219
  end
203
220
 
204
- def process(specs_string, from_adapter = false)
221
+ def process_specs(specs_string, from_adapter: false)
205
222
  if specs_string.nil?
206
223
  return false
207
224
  end
@@ -236,12 +253,33 @@ module Statsig
236
253
  @specs[:experiment_to_layer] = new_exp_to_layer
237
254
 
238
255
  unless from_adapter
239
- save_to_storage_adapter(specs_string)
256
+ save_config_specs_to_storage_adapter(specs_string)
240
257
  end
241
258
  true
242
259
  end
243
260
 
244
- def get_id_lists(init_diagnostics = nil)
261
+ def get_id_lists_from_adapter(init_diagnostics = nil)
262
+ init_diagnostics&.mark("get_id_lists", "start", "fetch_from_adapter")
263
+ cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
264
+ return if cached_values.nil?
265
+
266
+ init_diagnostics&.mark("get_id_lists", "end", "fetch_from_adapter", true)
267
+ id_lists = JSON.parse(cached_values)
268
+ process_id_lists(id_lists, init_diagnostics, from_adapter: true)
269
+ rescue StandardError
270
+ # Fallback to network
271
+ init_diagnostics&.mark("get_id_lists", "end", "fetch_from_adapter", false)
272
+ get_id_lists_from_network(init_diagnostics)
273
+ end
274
+
275
+ def save_id_lists_to_adapter(id_lists)
276
+ if @options.data_store.nil?
277
+ return
278
+ end
279
+ @options.data_store.set(Interfaces::IDataStore::CONFIG_SPECS_KEY, JSON.generate(id_lists))
280
+ end
281
+
282
+ def get_id_lists_from_network(init_diagnostics = nil)
245
283
  init_diagnostics&.mark("get_id_lists", "start", "network_request")
246
284
  response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
247
285
  if !e.nil? || response.nil?
@@ -251,69 +289,91 @@ module Statsig
251
289
 
252
290
  begin
253
291
  server_id_lists = JSON.parse(response)
254
- local_id_lists = @specs[:id_lists]
255
- if !server_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
256
- return
257
- end
258
- tasks = []
259
-
260
- if server_id_lists.length == 0
261
- return
262
- end
292
+ process_id_lists(server_id_lists, init_diagnostics)
293
+ rescue
294
+ # Ignored, will try again
295
+ end
296
+ end
263
297
 
264
- init_diagnostics&.mark("get_id_lists", "start", "process", server_id_lists.length)
298
+ def process_id_lists(new_id_lists, init_diagnostics, from_adapter: false)
299
+ local_id_lists = @specs[:id_lists]
300
+ if !new_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
301
+ return
302
+ end
303
+ tasks = []
265
304
 
266
- server_id_lists.each do |list_name, list|
267
- server_list = IDList.new(list)
268
- local_list = get_id_list(list_name)
305
+ if new_id_lists.length == 0
306
+ return
307
+ end
269
308
 
270
- unless local_list.is_a? IDList
271
- local_list = IDList.new(list)
272
- local_list.size = 0
273
- local_id_lists[list_name] = local_list
274
- end
309
+ init_diagnostics&.mark("get_id_lists", "start", "process", new_id_lists.length)
275
310
 
276
- # skip if server list is invalid
277
- if server_list.url.nil? || server_list.creation_time < local_list.creation_time || server_list.file_id.nil?
278
- next
279
- end
311
+ new_id_lists.each do |list_name, list|
312
+ new_list = IDList.new(list)
313
+ local_list = get_id_list(list_name)
280
314
 
281
- # reset local list if server list returns a newer file
282
- if server_list.file_id != local_list.file_id && server_list.creation_time >= local_list.creation_time
283
- local_list = IDList.new(list)
284
- local_list.size = 0
285
- local_id_lists[list_name] = local_list
286
- end
315
+ unless local_list.is_a? IDList
316
+ local_list = IDList.new(list)
317
+ local_list.size = 0
318
+ local_id_lists[list_name] = local_list
319
+ end
287
320
 
288
- # skip if server list is no bigger than local list, which means nothing new to read
289
- if server_list.size <= local_list.size
290
- next
291
- end
321
+ # skip if server list is invalid
322
+ if new_list.url.nil? || new_list.creation_time < local_list.creation_time || new_list.file_id.nil?
323
+ next
324
+ end
292
325
 
293
- tasks << Concurrent::Promise.execute(:executor => @id_list_thread_pool) do
294
- download_single_id_list(local_list)
295
- end
326
+ # reset local list if server list returns a newer file
327
+ if new_list.file_id != local_list.file_id && new_list.creation_time >= local_list.creation_time
328
+ local_list = IDList.new(list)
329
+ local_list.size = 0
330
+ local_id_lists[list_name] = local_list
296
331
  end
297
332
 
298
- result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
299
- if result.state != :fulfilled
300
- init_diagnostics&.mark("get_id_lists", "end", "process", false)
301
- return # timed out
333
+ # skip if server list is no bigger than local list, which means nothing new to read
334
+ if new_list.size <= local_list.size
335
+ next
302
336
  end
303
337
 
304
- delete_lists = []
305
- local_id_lists.each do |list_name, list|
306
- unless server_id_lists.key? list_name
307
- delete_lists.push list_name
338
+ tasks << Concurrent::Promise.execute(:executor => @id_list_thread_pool) do
339
+ if from_adapter
340
+ get_single_id_list_from_adapter(local_list)
341
+ else
342
+ download_single_id_list(local_list)
308
343
  end
309
344
  end
310
- delete_lists.each do |list_name|
311
- local_id_lists.delete list_name
345
+ end
346
+
347
+ result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
348
+ if result.state != :fulfilled
349
+ init_diagnostics&.mark("get_id_lists", "end", "process", false)
350
+ return # timed out
351
+ end
352
+
353
+ delete_lists = []
354
+ local_id_lists.each do |list_name, list|
355
+ unless new_id_lists.key? list_name
356
+ delete_lists.push list_name
312
357
  end
313
- init_diagnostics&.mark("get_id_lists", "end", "process", true)
314
- rescue
315
- # Ignored, will try again
316
358
  end
359
+ delete_lists.each do |list_name|
360
+ local_id_lists.delete list_name
361
+ end
362
+ init_diagnostics&.mark("get_id_lists", "end", "process", true)
363
+ end
364
+
365
+ def get_single_id_list_from_adapter(list)
366
+ cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
367
+ content = cached_values.to_s
368
+ process_single_id_list(list, content)
369
+ rescue StandardError
370
+ nil
371
+ end
372
+
373
+ def save_single_id_list_to_adapter(name, content)
374
+ return if @options.data_store.nil?
375
+
376
+ @options.data_store.set("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{name}", content)
317
377
  end
318
378
 
319
379
  def download_single_id_list(list)
@@ -325,9 +385,19 @@ module Statsig
325
385
  content_length = Integer(res['content-length'])
326
386
  nil if content_length.nil? || content_length <= 0
327
387
  content = res.body.to_s
388
+ success = process_single_id_list(list, content, content_length)
389
+ save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
390
+ rescue
391
+ nil
392
+ end
393
+ end
394
+
395
+ def process_single_id_list(list, content, content_length = nil)
396
+ false unless list.is_a? IDList
397
+ begin
328
398
  unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
329
399
  @specs[:id_lists].delete(list.name)
330
- return
400
+ return false
331
401
  end
332
402
  ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
333
403
  lines = content.split(/\r?\n/)
@@ -343,9 +413,14 @@ module Statsig
343
413
  end
344
414
  end
345
415
  list.ids = ids_clone
346
- list.size = list.size + content_length
416
+ list.size = if content_length.nil?
417
+ list.size + content.bytesize
418
+ else
419
+ list.size + content_length
420
+ end
421
+ return true
347
422
  rescue
348
- nil
423
+ return false
349
424
  end
350
425
  end
351
426
  end
data/lib/statsig.rb CHANGED
@@ -227,7 +227,7 @@ module Statsig
227
227
  def self.get_statsig_metadata
228
228
  {
229
229
  'sdkType' => 'ruby-server',
230
- 'sdkVersion' => '1.21.0',
230
+ 'sdkVersion' => '1.23.0',
231
231
  }
232
232
  end
233
233
 
@@ -36,7 +36,7 @@ class StatsigDriver
36
36
  @options = options || StatsigOptions.new
37
37
  @shutdown = false
38
38
  @secret_key = secret_key
39
- @net = Statsig::Network.new(secret_key, @options.api_url_base, @options.local_mode)
39
+ @net = Statsig::Network.new(secret_key, @options)
40
40
  @logger = Statsig::StatsigLogger.new(@net, @options)
41
41
  @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @init_diagnostics)
42
42
  @init_diagnostics.mark("overall", "end")
@@ -75,6 +75,10 @@ class StatsigOptions
75
75
  # default: false
76
76
  attr_accessor :disable_sorbet_logging_handlers
77
77
 
78
+ sig { returns(T.any(Integer, NilClass)) }
79
+ # Number of seconds before a network call is timed out
80
+ attr_accessor :network_timeout
81
+
78
82
  sig do
79
83
  params(
80
84
  environment: T.any(T::Hash[String, String], NilClass),
@@ -89,7 +93,8 @@ class StatsigOptions
89
93
  data_store: T.any(Statsig::Interfaces::IDataStore, NilClass),
90
94
  idlist_threadpool_size: Integer,
91
95
  disable_diagnostics_logging: T::Boolean,
92
- disable_sorbet_logging_handlers: T::Boolean
96
+ disable_sorbet_logging_handlers: T::Boolean,
97
+ network_timeout: T.any(Integer, NilClass)
93
98
  ).void
94
99
  end
95
100
 
@@ -106,7 +111,8 @@ class StatsigOptions
106
111
  data_store: nil,
107
112
  idlist_threadpool_size: 3,
108
113
  disable_diagnostics_logging: false,
109
- disable_sorbet_logging_handlers: false)
114
+ disable_sorbet_logging_handlers: false,
115
+ network_timeout: nil)
110
116
  @environment = environment.is_a?(Hash) ? environment : nil
111
117
  @api_url_base = api_url_base
112
118
  @rulesets_sync_interval = rulesets_sync_interval
@@ -120,5 +126,6 @@ class StatsigOptions
120
126
  @idlist_threadpool_size = idlist_threadpool_size
121
127
  @disable_diagnostics_logging = disable_diagnostics_logging
122
128
  @disable_sorbet_logging_handlers = disable_sorbet_logging_handlers
129
+ @network_timeout = network_timeout
123
130
  end
124
131
  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.21.0
4
+ version: 1.23.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: 2023-01-31 00:00:00.000000000 Z
11
+ date: 2023-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,34 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: 0.4.27
97
+ - !ruby/object:Gem::Dependency
98
+ name: sinatra
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: puma
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '6.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '6.0'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: user_agent_parser
99
127
  requirement: !ruby/object:Gem::Requirement
@@ -215,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
243
  - !ruby/object:Gem::Version
216
244
  version: '0'
217
245
  requirements: []
218
- rubygems_version: 3.4.1
246
+ rubygems_version: 3.3.7
219
247
  signing_key:
220
248
  specification_version: 4
221
249
  summary: Statsig server SDK for Ruby