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 +4 -4
- data/lib/interfaces/data_store.rb +11 -0
- data/lib/network.rb +8 -3
- data/lib/spec_store.rb +145 -70
- data/lib/statsig.rb +1 -1
- data/lib/statsig_driver.rb +1 -1
- data/lib/statsig_options.rb +9 -2
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89aaef291fd8cbd82af8bcf380894ea554588988b2de66507482420b475c8367
|
4
|
+
data.tar.gz: 3f535a1ce2a9af6e811c77df334b3b58a3c2503857f74bbf5b280764cfb041f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,
|
23
|
+
sig { params(server_secret: String, options: StatsigOptions, backoff_mult: Integer).void }
|
24
24
|
|
25
|
-
def initialize(server_secret,
|
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
|
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
|
-
|
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
|
-
|
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
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
256
|
+
save_config_specs_to_storage_adapter(specs_string)
|
240
257
|
end
|
241
258
|
true
|
242
259
|
end
|
243
260
|
|
244
|
-
def
|
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
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
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
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
305
|
+
if new_id_lists.length == 0
|
306
|
+
return
|
307
|
+
end
|
269
308
|
|
270
|
-
|
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
|
-
|
277
|
-
|
278
|
-
|
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
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
299
|
-
if
|
300
|
-
|
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
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
311
|
-
|
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 =
|
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
|
-
|
423
|
+
return false
|
349
424
|
end
|
350
425
|
end
|
351
426
|
end
|
data/lib/statsig.rb
CHANGED
data/lib/statsig_driver.rb
CHANGED
@@ -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
|
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")
|
data/lib/statsig_options.rb
CHANGED
@@ -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.
|
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-
|
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.
|
246
|
+
rubygems_version: 3.3.7
|
219
247
|
signing_key:
|
220
248
|
specification_version: 4
|
221
249
|
summary: Statsig server SDK for Ruby
|