statsig 1.22.0 → 1.24.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: 87f81b59161aaf7f035b0822858db6528c4268dddddaa46f012b6da61017c366
4
- data.tar.gz: 0c4b7ce02036994d5f3afcf1e1ecda13df7a3541b0bb1f6441996842cd91614f
3
+ metadata.gz: d0f95c8f4ce514c74570b095d81959705153201640821ba25179d24cd34ed4ec
4
+ data.tar.gz: c162910dc417b4f7311d91665d7240956a2eb1b58040050503bb8bb22b19cf12
5
5
  SHA512:
6
- metadata.gz: 344af0408c2136fa81acfe1f30258cd30b9ded6c472d0c1092583888793f9b998c303c77a86c99ff7484ceab6e3f2bdf69cf02ba17160208ca5ea2cbece562a0
7
- data.tar.gz: 75a9cd2967a25295b062f68279a7e6b0e0da4e59d7aeacaafbd9261605dce4ee9795633b9631c00008bace9395350210978b882edcc0364ecc3a1b80883c819c
6
+ metadata.gz: b1e085f8fbdcfb6020060f331b53767a68bda85f24669b6f710701ca0e9562ff4ca2fa873cce2a0917f48e64767f87c1489e1c792b695933c31a7ed8b71efd17
7
+ data.tar.gz: 9cf5e1ead6e76546d24d6042b2fe5f7dcbc5d1f9eae5423b4c7bca839174c1c8992d4c04ce61e21b53162872c99a619f86ea2b8c01f3d8485d9a35b67bcdf2e9
data/lib/config_result.rb CHANGED
@@ -11,6 +11,8 @@ module Statsig
11
11
  attr_accessor :explicit_parameters
12
12
  attr_accessor :is_experiment_group
13
13
  attr_accessor :evaluation_details
14
+ attr_accessor :group_name
15
+ attr_accessor :id_type
14
16
 
15
17
  def initialize(
16
18
  name,
@@ -21,7 +23,9 @@ module Statsig
21
23
  config_delegate = '',
22
24
  explicit_parameters = [],
23
25
  is_experiment_group: false,
24
- evaluation_details: nil)
26
+ evaluation_details: nil,
27
+ group_name: nil,
28
+ id_type: '')
25
29
  @name = name
26
30
  @gate_value = gate_value
27
31
  @json_value = json_value
@@ -32,6 +36,8 @@ module Statsig
32
36
  @explicit_parameters = explicit_parameters
33
37
  @is_experiment_group = is_experiment_group
34
38
  @evaluation_details = evaluation_details
39
+ @group_name = group_name
40
+ @id_type = id_type
35
41
  end
36
42
  end
37
43
  end
@@ -20,11 +20,19 @@ class DynamicConfig
20
20
  sig { returns(String) }
21
21
  attr_accessor :rule_id
22
22
 
23
- sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String).void }
24
- def initialize(name, value = {}, rule_id = '')
23
+ sig { returns(T.nilable(String)) }
24
+ attr_accessor :group_name
25
+
26
+ sig { returns(String) }
27
+ attr_accessor :id_type
28
+
29
+ sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String, group_name: T.nilable(String), id_type: String).void }
30
+ def initialize(name, value = {}, rule_id = '', group_name = nil, id_type = '')
25
31
  @name = name
26
32
  @value = value
27
33
  @rule_id = rule_id
34
+ @group_name = group_name
35
+ @id_type = id_type
28
36
  end
29
37
 
30
38
  sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
data/lib/evaluator.rb CHANGED
@@ -53,14 +53,20 @@ module Statsig
53
53
  end
54
54
 
55
55
  def get_config(user, config_name)
56
- if @config_overrides.has_key?(config_name)
56
+ if @config_overrides.key?(config_name)
57
+ id_type = @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name)['idType'] : ''
57
58
  return Statsig::ConfigResult.new(
58
59
  config_name,
59
60
  false,
60
61
  @config_overrides[config_name],
61
62
  'override',
62
63
  [],
63
- evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
64
+ evaluation_details: EvaluationDetails.local_override(
65
+ @spec_store.last_config_sync_time,
66
+ @spec_store.initial_config_sync_time
67
+ ),
68
+ id_type: id_type
69
+ )
64
70
  end
65
71
 
66
72
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
@@ -68,7 +74,13 @@ module Statsig
68
74
  end
69
75
 
70
76
  unless @spec_store.has_config?(config_name)
71
- return Statsig::ConfigResult.new(config_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
77
+ return Statsig::ConfigResult.new(
78
+ config_name,
79
+ evaluation_details: EvaluationDetails.unrecognized(
80
+ @spec_store.last_config_sync_time,
81
+ @spec_store.initial_config_sync_time
82
+ )
83
+ )
72
84
  end
73
85
 
74
86
  eval_spec(user, @spec_store.get_config(config_name))
@@ -159,8 +171,14 @@ module Statsig
159
171
  pass ? result.json_value : config['defaultValue'],
160
172
  result.rule_id,
161
173
  exposures,
162
- evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason),
174
+ evaluation_details: EvaluationDetails.new(
175
+ @spec_store.last_config_sync_time,
176
+ @spec_store.initial_config_sync_time,
177
+ @spec_store.init_reason
178
+ ),
163
179
  is_experiment_group: result.is_experiment_group,
180
+ group_name: result.group_name,
181
+ id_type: config['idType']
164
182
  )
165
183
  end
166
184
 
@@ -176,7 +194,14 @@ module Statsig
176
194
  config['defaultValue'],
177
195
  default_rule_id,
178
196
  exposures,
179
- evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason))
197
+ evaluation_details: EvaluationDetails.new(
198
+ @spec_store.last_config_sync_time,
199
+ @spec_store.initial_config_sync_time,
200
+ @spec_store.init_reason
201
+ ),
202
+ group_name: nil,
203
+ id_type: config['idType']
204
+ )
180
205
  end
181
206
 
182
207
  private
@@ -206,8 +231,14 @@ module Statsig
206
231
  rule['returnValue'],
207
232
  rule['id'],
208
233
  exposures,
209
- evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason),
210
- is_experiment_group: rule["isExperimentGroup"] == true)
234
+ evaluation_details: EvaluationDetails.new(
235
+ @spec_store.last_config_sync_time,
236
+ @spec_store.initial_config_sync_time,
237
+ @spec_store.init_reason
238
+ ),
239
+ is_experiment_group: rule["isExperimentGroup"] == true,
240
+ group_name: rule['groupName']
241
+ )
211
242
  end
212
243
 
213
244
  def eval_delegate(name, user, rule, exposures)
@@ -3,6 +3,7 @@ module Statsig
3
3
  module Interfaces
4
4
  class IDataStore
5
5
  CONFIG_SPECS_KEY = "statsig.cache"
6
+ ID_LISTS_KEY = "statsig.id_lists"
6
7
 
7
8
  def init
8
9
  end
data/lib/spec_store.rb CHANGED
@@ -42,7 +42,7 @@ module Statsig
42
42
  puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
43
43
  else
44
44
  init_diagnostics&.mark("bootstrap", "start", "load")
45
- if process(options.bootstrap_values)
45
+ if process_specs(options.bootstrap_values)
46
46
  @init_reason = EvaluationReason::BOOTSTRAP
47
47
  end
48
48
  init_diagnostics&.mark("bootstrap", "end", "load", @init_reason == EvaluationReason::BOOTSTRAP)
@@ -55,7 +55,7 @@ module Statsig
55
55
  unless @options.data_store.nil?
56
56
  init_diagnostics&.mark("data_store", "start", "load")
57
57
  @options.data_store.init
58
- load_from_storage_adapter
58
+ load_config_specs_from_storage_adapter(init_diagnostics: init_diagnostics)
59
59
  init_diagnostics&.mark("data_store", "end", "load", @init_reason == EvaluationReason::DATA_ADAPTER)
60
60
  end
61
61
 
@@ -64,7 +64,11 @@ module Statsig
64
64
  end
65
65
 
66
66
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
67
- 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
68
72
 
69
73
  @config_sync_thread = sync_config_specs
70
74
  @id_lists_sync_thread = sync_id_lists
@@ -130,16 +134,23 @@ module Statsig
130
134
 
131
135
  private
132
136
 
133
- def load_from_storage_adapter
137
+ def load_config_specs_from_storage_adapter(init_diagnostics: nil)
138
+ init_diagnostics&.mark("download_config_specs", "start", "fetch_from_adapter")
134
139
  cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
135
- if cached_values.nil?
136
- return
137
- end
138
- process(cached_values, true)
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)
139
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)
140
151
  end
141
152
 
142
- def save_to_storage_adapter(specs_string)
153
+ def save_config_specs_to_storage_adapter(specs_string)
143
154
  if @options.data_store.nil?
144
155
  return
145
156
  end
@@ -151,7 +162,7 @@ module Statsig
151
162
  loop do
152
163
  sleep @options.rulesets_sync_interval
153
164
  if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
154
- load_from_storage_adapter
165
+ load_config_specs_from_storage_adapter
155
166
  else
156
167
  download_config_specs
157
168
  end
@@ -163,7 +174,11 @@ module Statsig
163
174
  Thread.new do
164
175
  loop do
165
176
  sleep @id_lists_sync_interval
166
- 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
167
182
  end
168
183
  end
169
184
  end
@@ -184,7 +199,7 @@ module Statsig
184
199
  unless response.nil?
185
200
  init_diagnostics&.mark("download_config_specs", "start", "process")
186
201
 
187
- if process(response.body)
202
+ if process_specs(response.body)
188
203
  @init_reason = EvaluationReason::NETWORK
189
204
  @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
190
205
  end
@@ -203,7 +218,7 @@ module Statsig
203
218
  @error_callback.call(error) unless error.nil? or @error_callback.nil?
204
219
  end
205
220
 
206
- def process(specs_string, from_adapter = false)
221
+ def process_specs(specs_string, from_adapter: false)
207
222
  if specs_string.nil?
208
223
  return false
209
224
  end
@@ -238,12 +253,33 @@ module Statsig
238
253
  @specs[:experiment_to_layer] = new_exp_to_layer
239
254
 
240
255
  unless from_adapter
241
- save_to_storage_adapter(specs_string)
256
+ save_config_specs_to_storage_adapter(specs_string)
242
257
  end
243
258
  true
244
259
  end
245
260
 
246
- 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)
247
283
  init_diagnostics&.mark("get_id_lists", "start", "network_request")
248
284
  response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
249
285
  if !e.nil? || response.nil?
@@ -253,69 +289,91 @@ module Statsig
253
289
 
254
290
  begin
255
291
  server_id_lists = JSON.parse(response)
256
- local_id_lists = @specs[:id_lists]
257
- if !server_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
258
- return
259
- end
260
- tasks = []
261
-
262
- if server_id_lists.length == 0
263
- return
264
- end
292
+ process_id_lists(server_id_lists, init_diagnostics)
293
+ rescue
294
+ # Ignored, will try again
295
+ end
296
+ end
265
297
 
266
- 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 = []
267
304
 
268
- server_id_lists.each do |list_name, list|
269
- server_list = IDList.new(list)
270
- local_list = get_id_list(list_name)
305
+ if new_id_lists.length == 0
306
+ return
307
+ end
271
308
 
272
- unless local_list.is_a? IDList
273
- local_list = IDList.new(list)
274
- local_list.size = 0
275
- local_id_lists[list_name] = local_list
276
- end
309
+ init_diagnostics&.mark("get_id_lists", "start", "process", new_id_lists.length)
277
310
 
278
- # skip if server list is invalid
279
- if server_list.url.nil? || server_list.creation_time < local_list.creation_time || server_list.file_id.nil?
280
- next
281
- end
311
+ new_id_lists.each do |list_name, list|
312
+ new_list = IDList.new(list)
313
+ local_list = get_id_list(list_name)
282
314
 
283
- # reset local list if server list returns a newer file
284
- if server_list.file_id != local_list.file_id && server_list.creation_time >= local_list.creation_time
285
- local_list = IDList.new(list)
286
- local_list.size = 0
287
- local_id_lists[list_name] = local_list
288
- 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
289
320
 
290
- # skip if server list is no bigger than local list, which means nothing new to read
291
- if server_list.size <= local_list.size
292
- next
293
- 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
294
325
 
295
- tasks << Concurrent::Promise.execute(:executor => @id_list_thread_pool) do
296
- download_single_id_list(local_list)
297
- 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
298
331
  end
299
332
 
300
- result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
301
- if result.state != :fulfilled
302
- init_diagnostics&.mark("get_id_lists", "end", "process", false)
303
- 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
304
336
  end
305
337
 
306
- delete_lists = []
307
- local_id_lists.each do |list_name, list|
308
- unless server_id_lists.key? list_name
309
- 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)
310
343
  end
311
344
  end
312
- delete_lists.each do |list_name|
313
- 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
314
357
  end
315
- init_diagnostics&.mark("get_id_lists", "end", "process", true)
316
- rescue
317
- # Ignored, will try again
318
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)
319
377
  end
320
378
 
321
379
  def download_single_id_list(list)
@@ -327,9 +385,19 @@ module Statsig
327
385
  content_length = Integer(res['content-length'])
328
386
  nil if content_length.nil? || content_length <= 0
329
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
330
398
  unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
331
399
  @specs[:id_lists].delete(list.name)
332
- return
400
+ return false
333
401
  end
334
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
335
403
  lines = content.split(/\r?\n/)
@@ -345,9 +413,14 @@ module Statsig
345
413
  end
346
414
  end
347
415
  list.ids = ids_clone
348
- 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
349
422
  rescue
350
- nil
423
+ return false
351
424
  end
352
425
  end
353
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.24.0',
231
231
  }
232
232
  end
233
233
 
@@ -245,7 +245,7 @@ class StatsigDriver
245
245
  end
246
246
  end
247
247
 
248
- DynamicConfig.new(res.name, res.json_value, res.rule_id)
248
+ DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type)
249
249
  end
250
250
 
251
251
  def validate_user(user)
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.22.0
4
+ version: 1.24.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-02-02 00:00:00.000000000 Z
11
+ date: 2023-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler