statsig 1.25.1 → 1.26.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.
data/lib/spec_store.rb CHANGED
@@ -12,7 +12,7 @@ module Statsig
12
12
  attr_accessor :initial_config_sync_time
13
13
  attr_accessor :init_reason
14
14
 
15
- def initialize(network, options, error_callback, init_diagnostics = nil)
15
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
16
16
  @init_reason = EvaluationReason::UNINITIALIZED
17
17
  @network = network
18
18
  @options = options
@@ -27,8 +27,12 @@ module Statsig
27
27
  :configs => {},
28
28
  :layers => {},
29
29
  :id_lists => {},
30
- :experiment_to_layer => {}
30
+ :experiment_to_layer => {},
31
+ :sdk_keys_to_app_ids => {}
31
32
  }
33
+ @diagnostics = diagnostics
34
+ @error_boundary = error_boundary
35
+ @logger = logger
32
36
 
33
37
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
34
38
  options.idlist_threadpool_size,
@@ -38,37 +42,36 @@ module Statsig
38
42
  )
39
43
 
40
44
  unless @options.bootstrap_values.nil?
41
- begin
42
- if !@options.data_store.nil?
43
- puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
44
- else
45
- init_diagnostics&.mark("bootstrap", "start", "load")
45
+ if !@options.data_store.nil?
46
+ puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
47
+ else
48
+ tracker = @diagnostics.track('bootstrap', 'process')
49
+ begin
46
50
  if process_specs(options.bootstrap_values)
47
51
  @init_reason = EvaluationReason::BOOTSTRAP
48
52
  end
49
- init_diagnostics&.mark("bootstrap", "end", "load", @init_reason == EvaluationReason::BOOTSTRAP)
53
+ rescue
54
+ puts 'the provided bootstrapValues is not a valid JSON string'
55
+ ensure
56
+ tracker.end(success: @init_reason == EvaluationReason::BOOTSTRAP)
50
57
  end
51
- rescue
52
- puts 'the provided bootstrapValues is not a valid JSON string'
53
58
  end
54
59
  end
55
60
 
56
61
  unless @options.data_store.nil?
57
- init_diagnostics&.mark("data_store", "start", "load")
58
62
  @options.data_store.init
59
- load_config_specs_from_storage_adapter(init_diagnostics: init_diagnostics)
60
- init_diagnostics&.mark("data_store", "end", "load", @init_reason == EvaluationReason::DATA_ADAPTER)
63
+ load_config_specs_from_storage_adapter
61
64
  end
62
65
 
63
66
  if @init_reason == EvaluationReason::UNINITIALIZED
64
- download_config_specs(init_diagnostics)
67
+ download_config_specs
65
68
  end
66
69
 
67
70
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
68
71
  if !@options.data_store.nil?
69
- get_id_lists_from_adapter(init_diagnostics)
72
+ get_id_lists_from_adapter
70
73
  else
71
- get_id_lists_from_network(init_diagnostics)
74
+ get_id_lists_from_network
72
75
  end
73
76
 
74
77
  @config_sync_thread = sync_config_specs
@@ -120,35 +123,47 @@ module Statsig
120
123
  @specs[:id_lists][list_name]
121
124
  end
122
125
 
126
+ def has_sdk_key?(sdk_key)
127
+ @specs[:sdk_keys_to_app_ids].key?(sdk_key)
128
+ end
129
+
130
+ def get_app_id_for_sdk_key(sdk_key)
131
+ if sdk_key.nil?
132
+ return nil
133
+ end
134
+ return nil unless has_sdk_key?(sdk_key)
135
+ @specs[:sdk_keys_to_app_ids][sdk_key]
136
+ end
137
+
123
138
  def get_raw_specs
124
139
  @specs
125
140
  end
126
141
 
127
142
  def maybe_restart_background_threads
128
- if @config_sync_thread.nil? or !@config_sync_thread.alive?
143
+ if @config_sync_thread.nil? || !@config_sync_thread.alive?
129
144
  @config_sync_thread = sync_config_specs
130
145
  end
131
- if @id_lists_sync_thread.nil? or !@id_lists_sync_thread.alive?
146
+ if @id_lists_sync_thread.nil? || !@id_lists_sync_thread.alive?
132
147
  @id_lists_sync_thread = sync_id_lists
133
148
  end
134
149
  end
135
150
 
136
151
  private
137
152
 
138
- def load_config_specs_from_storage_adapter(init_diagnostics: nil)
139
- init_diagnostics&.mark("download_config_specs", "start", "fetch_from_adapter")
153
+ def load_config_specs_from_storage_adapter
154
+ tracker = @diagnostics.track('data_store_config_specs', 'fetch')
140
155
  cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
141
- init_diagnostics&.mark("download_config_specs", "end", "fetch_from_adapter", true)
156
+ tracker.end(success: true)
142
157
  return if cached_values.nil?
143
158
 
144
- init_diagnostics&.mark("download_config_specs", "start", "process")
159
+ tracker = @diagnostics.track('data_store_config_specs', 'process')
145
160
  process_specs(cached_values, from_adapter: true)
146
161
  @init_reason = EvaluationReason::DATA_ADAPTER
147
- init_diagnostics&.mark("download_config_specs", "end", "process", @init_reason)
162
+ tracker.end(success: true)
148
163
  rescue StandardError
149
164
  # Fallback to network
150
- init_diagnostics&.mark("download_config_specs", "end", "fetch_from_adapter", false)
151
- download_config_specs(init_diagnostics)
165
+ tracker.end(success: false)
166
+ download_config_specs
152
167
  end
153
168
 
154
169
  def save_config_specs_to_storage_adapter(specs_string)
@@ -160,32 +175,40 @@ module Statsig
160
175
 
161
176
  def sync_config_specs
162
177
  Thread.new do
163
- loop do
164
- sleep @options.rulesets_sync_interval
165
- if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
166
- load_config_specs_from_storage_adapter
167
- else
168
- download_config_specs
178
+ @error_boundary.capture(task: lambda {
179
+ loop do
180
+ @diagnostics = Diagnostics.new('config_sync')
181
+ sleep @options.rulesets_sync_interval
182
+ if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
183
+ load_config_specs_from_storage_adapter
184
+ else
185
+ download_config_specs
186
+ end
187
+ @logger.log_diagnostics_event(@diagnostics)
169
188
  end
170
- end
189
+ })
171
190
  end
172
191
  end
173
192
 
174
193
  def sync_id_lists
175
194
  Thread.new do
176
- loop do
177
- sleep @id_lists_sync_interval
178
- if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
179
- get_id_lists_from_adapter
180
- else
181
- get_id_lists_from_network
195
+ @error_boundary.capture(task: lambda {
196
+ loop do
197
+ @diagnostics = Diagnostics.new('config_sync')
198
+ sleep @id_lists_sync_interval
199
+ if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
200
+ get_id_lists_from_adapter
201
+ else
202
+ get_id_lists_from_network
203
+ end
204
+ @logger.log_diagnostics_event(@diagnostics)
182
205
  end
183
- end
206
+ })
184
207
  end
185
208
  end
186
209
 
187
- def download_config_specs(init_diagnostics = nil)
188
- init_diagnostics&.mark("download_config_specs", "start", "network_request")
210
+ def download_config_specs
211
+ tracker = @diagnostics.track('download_config_specs', 'network_request')
189
212
 
190
213
  error = nil
191
214
  begin
@@ -194,18 +217,17 @@ module Statsig
194
217
  if e.is_a? NetworkError
195
218
  code = e.http_code
196
219
  end
197
- init_diagnostics&.mark("download_config_specs", "end", "network_request", code)
220
+ tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
198
221
 
199
222
  if e.nil?
200
223
  unless response.nil?
201
- init_diagnostics&.mark("download_config_specs", "start", "process")
202
-
224
+ tracker = @diagnostics.track('download_config_specs', 'process')
203
225
  if process_specs(response.body.to_s)
204
226
  @init_reason = EvaluationReason::NETWORK
205
- @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
206
227
  end
228
+ tracker.end(success: @init_reason == EvaluationReason::NETWORK)
207
229
 
208
- init_diagnostics&.mark("download_config_specs", "end", "process", @init_reason == EvaluationReason::NETWORK)
230
+ @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
209
231
  end
210
232
 
211
233
  nil
@@ -252,6 +274,9 @@ module Statsig
252
274
  @specs[:configs] = new_configs
253
275
  @specs[:layers] = new_layers
254
276
  @specs[:experiment_to_layer] = new_exp_to_layer
277
+ @specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
278
+
279
+ specs_json['diagnostics']
255
280
 
256
281
  unless from_adapter
257
282
  save_config_specs_to_storage_adapter(specs_string)
@@ -259,18 +284,18 @@ module Statsig
259
284
  true
260
285
  end
261
286
 
262
- def get_id_lists_from_adapter(init_diagnostics = nil)
263
- init_diagnostics&.mark("get_id_lists", "start", "fetch_from_adapter")
287
+ def get_id_lists_from_adapter
288
+ tracker = @diagnostics.track('data_store_id_lists', 'fetch')
264
289
  cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
265
290
  return if cached_values.nil?
266
291
 
267
- init_diagnostics&.mark("get_id_lists", "end", "fetch_from_adapter", true)
292
+ tracker.end(success: true)
268
293
  id_lists = JSON.parse(cached_values)
269
- process_id_lists(id_lists, init_diagnostics, from_adapter: true)
294
+ process_id_lists(id_lists, from_adapter: true)
270
295
  rescue StandardError
271
296
  # Fallback to network
272
- init_diagnostics&.mark("get_id_lists", "end", "fetch_from_adapter", false)
273
- get_id_lists_from_network(init_diagnostics)
297
+ tracker.end(success: false)
298
+ get_id_lists_from_network
274
299
  end
275
300
 
276
301
  def save_id_lists_to_adapter(id_lists_raw_json)
@@ -280,36 +305,46 @@ module Statsig
280
305
  @options.data_store.set(Interfaces::IDataStore::ID_LISTS_KEY, id_lists_raw_json)
281
306
  end
282
307
 
283
- def get_id_lists_from_network(init_diagnostics = nil)
284
- init_diagnostics&.mark("get_id_lists", "start", "network_request")
308
+ def get_id_lists_from_network
309
+ tracker = @diagnostics.track('get_id_list_sources', 'network_request')
285
310
  response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
286
- if !e.nil? || response.nil?
311
+ code = response&.status.to_i
312
+ if e.is_a? NetworkError
313
+ code = e.http_code
314
+ end
315
+ success = e.nil? && !response.nil?
316
+ tracker.end(statusCode: code, success: success, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
317
+ if !success
287
318
  return
288
319
  end
289
- init_diagnostics&.mark("get_id_lists", "end", "network_request", response.status.to_i)
290
320
 
291
321
  begin
292
322
  server_id_lists = JSON.parse(response)
293
- process_id_lists(server_id_lists, init_diagnostics)
323
+ process_id_lists(server_id_lists)
294
324
  save_id_lists_to_adapter(response.body.to_s)
295
325
  rescue
296
326
  # Ignored, will try again
297
327
  end
298
328
  end
299
329
 
300
- def process_id_lists(new_id_lists, init_diagnostics, from_adapter: false)
330
+ def process_id_lists(new_id_lists, from_adapter: false)
301
331
  local_id_lists = @specs[:id_lists]
302
332
  if !new_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
303
333
  return
304
334
  end
305
335
  tasks = []
306
336
 
307
- if new_id_lists.length == 0
337
+ tracker = @diagnostics.track(
338
+ from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
339
+ 'process',
340
+ { idListCount: new_id_lists.length }
341
+ )
342
+
343
+ if new_id_lists.empty?
344
+ tracker.end(success: true)
308
345
  return
309
346
  end
310
347
 
311
- init_diagnostics&.mark("get_id_lists", "start", "process", new_id_lists.length)
312
-
313
348
  delete_lists = []
314
349
  local_id_lists.each do |list_name, list|
315
350
  unless new_id_lists.key? list_name
@@ -357,14 +392,17 @@ module Statsig
357
392
  end
358
393
 
359
394
  result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
360
- init_diagnostics&.mark("get_id_lists", "end", "process", result.state == :fulfilled)
395
+ tracker.end(success: result.state == :fulfilled)
361
396
  end
362
397
 
363
398
  def get_single_id_list_from_adapter(list)
399
+ tracker = @diagnostics.track('data_store_id_list', 'fetch', { url: list.url })
364
400
  cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
401
+ tracker.end(success: true)
365
402
  content = cached_values.to_s
366
- process_single_id_list(list, content)
403
+ process_single_id_list(list, content, from_adapter: true)
367
404
  rescue StandardError
405
+ tracker.end(success: false)
368
406
  nil
369
407
  end
370
408
 
@@ -377,8 +415,10 @@ module Statsig
377
415
  def download_single_id_list(list)
378
416
  nil unless list.is_a? IDList
379
417
  http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
418
+ tracker = @diagnostics.track('get_id_list', 'network_request', { url: list.url })
380
419
  begin
381
420
  res = http.get(list.url)
421
+ tracker.end(statusCode: res.status.code, success: res.status.success?)
382
422
  nil unless res.status.success?
383
423
  content_length = Integer(res['content-length'])
384
424
  nil if content_length.nil? || content_length <= 0
@@ -386,15 +426,18 @@ module Statsig
386
426
  success = process_single_id_list(list, content, content_length)
387
427
  save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
388
428
  rescue
429
+ tracker.end(success: false)
389
430
  nil
390
431
  end
391
432
  end
392
433
 
393
- def process_single_id_list(list, content, content_length = nil)
434
+ def process_single_id_list(list, content, content_length = nil, from_adapter: false)
394
435
  false unless list.is_a? IDList
395
436
  begin
437
+ tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
396
438
  unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
397
439
  @specs[:id_lists].delete(list.name)
440
+ tracker.end(success: false)
398
441
  return false
399
442
  end
400
443
  ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
@@ -416,8 +459,10 @@ module Statsig
416
459
  else
417
460
  list.size + content_length
418
461
  end
462
+ tracker.end(success: true)
419
463
  return true
420
464
  rescue
465
+ tracker.end(success: false)
421
466
  return false
422
467
  end
423
468
  end
data/lib/statsig.rb CHANGED
@@ -208,17 +208,19 @@ module Statsig
208
208
  @shared_instance&.override_config(config_name, config_value)
209
209
  end
210
210
 
211
- sig { params(user: StatsigUser).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
211
+ sig { params(user: StatsigUser, hash: String, client_sdk_key: T.any(String, NilClass)).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
212
212
  ##
213
213
  # Gets all evaluated values for the given user.
214
214
  # These values can then be given to a Statsig Client SDK via bootstrapping.
215
215
  #
216
216
  # @param user A StatsigUser object used for the evaluation
217
+ # @param hash The type of hashing algorithm to use ('sha256', 'djb2', 'none')
218
+ # @param client_sdk_key A optional client sdk key to be used for the evaluation
217
219
  #
218
220
  # @note See Ruby Documentation: https://docs.statsig.com/server/rubySDK)
219
- def self.get_client_initialize_response(user)
221
+ def self.get_client_initialize_response(user, hash: 'sha256', client_sdk_key: nil)
220
222
  ensure_initialized
221
- @shared_instance&.get_client_initialize_response(user)
223
+ @shared_instance&.get_client_initialize_response(user, hash, client_sdk_key)
222
224
  end
223
225
 
224
226
  sig { returns(T::Hash[String, String]) }
@@ -227,7 +229,7 @@ module Statsig
227
229
  def self.get_statsig_metadata
228
230
  {
229
231
  'sdkType' => 'ruby-server',
230
- 'sdkVersion' => '1.25.1',
232
+ 'sdkVersion' => '1.26.0',
231
233
  }
232
234
  end
233
235