statsig 1.25.1 → 1.25.2

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: f1772b0fb47eaaf4b95950425eeba70ea7b792a8e5aad952b67e7aea8000d06b
4
- data.tar.gz: 2e75c8c1c9e68817440dd73d98020f99f20e80255424fb499dff8d3464eb39ba
3
+ metadata.gz: c7f3c8522d7d8062daf248fd667fd0d8152a4194339fde799814a5da44812eda
4
+ data.tar.gz: 1fd5e67d4a75ab3b5b6c92c780841252a4610e18bc13310f9b11dbb583aaf16a
5
5
  SHA512:
6
- metadata.gz: 5f291605da74348e702b8258ca52c6bb609d6359c961a862d76c50e7a53b348bb7705ed6ac2e3da2b68ea89c2db852feb50f5213d5c9543af665142e8cc2bc15
7
- data.tar.gz: 3f1923846883e667b5f5ae14c9efd90fd295a3c0336be262ec879227b3af85b513f9dc407687e8f6273e911644b40bbde7324ff86fc3bc7b26e4a3fd9345992b
6
+ metadata.gz: 8f516ca6d221c9d789f1f358b550edeedf4203c01ead45374b97a3b81f4205d09f1c7c3e0214516cdcad314bacbdbde39a5df2053779679fe4df593c974c8300
7
+ data.tar.gz: a90846ef3c050c7c0b8bddbabd19ed7bff6236a15ed941f55d1a305f4c9cb5ff35f595a3d7ea4ab1560d7f7bae11c669c29d17f8041da9125524669703dc5370
data/lib/diagnostics.rb CHANGED
@@ -12,33 +12,92 @@ module Statsig
12
12
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
13
13
  attr_reader :markers
14
14
 
15
- sig { params(context: String).void }
16
-
17
15
  def initialize(context)
18
16
  @context = context
19
17
  @markers = []
20
18
  end
21
19
 
22
- sig { params(key: String, action: String, step: T.any(String, NilClass), value: T.any(String, Integer, T::Boolean, NilClass)).void }
20
+ sig do
21
+ params(
22
+ key: String,
23
+ action: String,
24
+ step: T.any(String, NilClass),
25
+ value: T.any(String, Integer, T::Boolean, NilClass),
26
+ metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
27
+ ).void
28
+ end
23
29
 
24
- def mark(key, action, step = nil, value = nil)
30
+ def mark(key, action, step = nil, value = nil, metadata = nil)
25
31
  @markers.push({
26
32
  key: key,
27
33
  step: step,
28
34
  action: action,
29
35
  value: value,
36
+ metadata: metadata,
30
37
  timestamp: (Time.now.to_f * 1000).to_i
31
38
  })
32
39
  end
33
40
 
41
+ sig do
42
+ params(
43
+ key: String,
44
+ step: T.any(String, NilClass),
45
+ value: T.any(String, Integer, T::Boolean, NilClass),
46
+ metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
47
+ ).returns(Tracker)
48
+ end
49
+ def track(key, step = nil, value = nil, metadata = nil)
50
+ tracker = Tracker.new(self, key, step, metadata)
51
+ tracker.start(value)
52
+ tracker
53
+ end
54
+
34
55
  sig { returns(T::Hash[Symbol, T.untyped]) }
35
56
 
36
57
  def serialize
37
58
  {
38
- context: @context,
39
- markers: @markers
59
+ context: @context.clone,
60
+ markers: @markers.clone
40
61
  }
41
62
  end
42
- end
43
63
 
44
- end
64
+ def clear_markers
65
+ @markers.clear
66
+ end
67
+
68
+ class Context
69
+ INITIALIZE = 'initialize'.freeze
70
+ CONFIG_SYNC = 'config_sync'.freeze
71
+ API_CALL = 'api_call'.freeze
72
+ end
73
+
74
+ API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
75
+
76
+ class Tracker
77
+ extend T::Sig
78
+
79
+ sig do
80
+ params(
81
+ diagnostics: Diagnostics,
82
+ key: String,
83
+ step: T.any(String, NilClass),
84
+ metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
85
+ ).void
86
+ end
87
+ def initialize(diagnostics, key, step, metadata)
88
+ @diagnostics = diagnostics
89
+ @key = key
90
+ @step = step
91
+ @metadata = metadata
92
+ end
93
+
94
+ def start(value = nil)
95
+ @diagnostics.mark(@key, 'start', @step, value, @metadata)
96
+ end
97
+
98
+ def end(value = nil)
99
+ @diagnostics.mark(@key, 'end', @step, value, @metadata)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,25 +1,46 @@
1
- require "statsig_errors"
1
+ # typed: true
2
+
3
+ require 'statsig_errors'
4
+ require 'sorbet-runtime'
2
5
 
3
6
  $endpoint = 'https://statsigapi.net/v1/sdk_exception'
4
7
 
5
8
  module Statsig
6
9
  class ErrorBoundary
10
+ extend T::Sig
11
+
12
+ sig { returns(T.any(StatsigLogger, NilClass)) }
13
+ attr_accessor :logger
14
+
15
+ sig { params(sdk_key: String).void }
7
16
  def initialize(sdk_key)
8
17
  @sdk_key = sdk_key
9
18
  @seen = Set.new
10
19
  end
11
20
 
12
- def capture(task, recover = -> {})
21
+ def sample_diagnostics
22
+ rand(10_000).zero?
23
+ end
24
+
25
+ def capture(task:, recover: -> {}, caller: nil)
26
+ if !caller.nil? && Diagnostics::API_CALL_KEYS.include?(caller) && sample_diagnostics
27
+ diagnostics = Diagnostics.new('api_call')
28
+ tracker = diagnostics.track(caller)
29
+ end
13
30
  begin
14
- return task.call
31
+ res = task.call
32
+ tracker&.end(true)
15
33
  rescue StandardError => e
34
+ tracker&.end(false)
16
35
  if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
17
36
  raise e
18
37
  end
19
- puts "[Statsig]: An unexpected exception occurred."
38
+ puts '[Statsig]: An unexpected exception occurred.'
20
39
  log_exception(e)
21
- return recover.call
40
+ res = recover.call
22
41
  end
42
+ @logger&.log_diagnostics_event(diagnostics)
43
+ return res
23
44
  end
24
45
 
25
46
  private
@@ -35,18 +56,18 @@ module Statsig
35
56
  meta = Statsig.get_statsig_metadata
36
57
  http = HTTP.headers(
37
58
  {
38
- "STATSIG-API-KEY" => @sdk_key,
39
- "STATSIG-SDK-TYPE" => meta['sdkType'],
40
- "STATSIG-SDK-VERSION" => meta['sdkVersion'],
41
- "Content-Type" => "application/json; charset=UTF-8"
59
+ 'STATSIG-API-KEY' => @sdk_key,
60
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
61
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
62
+ 'Content-Type' => 'application/json; charset=UTF-8'
42
63
  }).accept(:json)
43
64
  body = {
44
- "exception" => name,
45
- "info" => {
46
- "trace" => exception.backtrace.to_s,
47
- "message" => exception.message
65
+ 'exception' => name,
66
+ 'info' => {
67
+ 'trace' => exception.backtrace.to_s,
68
+ 'message' => exception.message
48
69
  }.to_s,
49
- "statsigMetadata" => meta
70
+ 'statsigMetadata' => meta
50
71
  }
51
72
  http.post($endpoint, body: JSON.generate(body))
52
73
  rescue
data/lib/evaluator.rb CHANGED
@@ -17,8 +17,8 @@ module Statsig
17
17
  class Evaluator
18
18
  attr_accessor :spec_store
19
19
 
20
- def initialize(network, options, error_callback, init_diagnostics = nil)
21
- @spec_store = Statsig::SpecStore.new(network, options, error_callback, init_diagnostics)
20
+ def initialize(network, options, error_callback, diagnostics)
21
+ @spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics)
22
22
  UAParser.initialize_async
23
23
  CountryLookup.initialize_async
24
24
 
data/lib/network.rb CHANGED
@@ -4,6 +4,7 @@ require 'http'
4
4
  require 'json'
5
5
  require 'securerandom'
6
6
  require 'sorbet-runtime'
7
+ require 'uri_helper'
7
8
 
8
9
  $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
9
10
 
@@ -24,12 +25,8 @@ module Statsig
24
25
 
25
26
  def initialize(server_secret, options, backoff_mult = 10)
26
27
  super()
27
- api = options.api_url_base
28
- unless api.end_with?('/')
29
- api += '/'
30
- end
28
+ URIHelper.initialize(options)
31
29
  @server_secret = server_secret
32
- @api = api
33
30
  @local_mode = options.local_mode
34
31
  @timeout = options.network_timeout
35
32
  @backoff_multiplier = backoff_mult
@@ -67,8 +64,9 @@ module Statsig
67
64
  backoff_adjusted = @post_logs_retry_backoff.call(retries)
68
65
  end
69
66
  end
67
+ url = URIHelper.build_url(endpoint)
70
68
  begin
71
- res = http.post(@api + endpoint, body: body)
69
+ res = http.post(url, body: body)
72
70
  rescue StandardError => e
73
71
  ## network error retry
74
72
  return nil, e unless retries > 0
@@ -76,7 +74,7 @@ module Statsig
76
74
  return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
77
75
  end
78
76
  return res, nil if res.status.success?
79
- return nil, NetworkError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
77
+ return nil, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
80
78
  ## status code retry
81
79
  sleep backoff_adjusted
82
80
  post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
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)
16
16
  @init_reason = EvaluationReason::UNINITIALIZED
17
17
  @network = network
18
18
  @options = options
@@ -29,6 +29,7 @@ module Statsig
29
29
  :id_lists => {},
30
30
  :experiment_to_layer => {}
31
31
  }
32
+ @diagnostics = diagnostics
32
33
 
33
34
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
34
35
  options.idlist_threadpool_size,
@@ -42,11 +43,11 @@ module Statsig
42
43
  if !@options.data_store.nil?
43
44
  puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
44
45
  else
45
- init_diagnostics&.mark("bootstrap", "start", "load")
46
+ tracker = @diagnostics.track('bootstrap', 'process')
46
47
  if process_specs(options.bootstrap_values)
47
48
  @init_reason = EvaluationReason::BOOTSTRAP
48
49
  end
49
- init_diagnostics&.mark("bootstrap", "end", "load", @init_reason == EvaluationReason::BOOTSTRAP)
50
+ tracker.end(@init_reason == EvaluationReason::BOOTSTRAP)
50
51
  end
51
52
  rescue
52
53
  puts 'the provided bootstrapValues is not a valid JSON string'
@@ -54,21 +55,19 @@ module Statsig
54
55
  end
55
56
 
56
57
  unless @options.data_store.nil?
57
- init_diagnostics&.mark("data_store", "start", "load")
58
58
  @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)
59
+ load_config_specs_from_storage_adapter
61
60
  end
62
61
 
63
62
  if @init_reason == EvaluationReason::UNINITIALIZED
64
- download_config_specs(init_diagnostics)
63
+ download_config_specs
65
64
  end
66
65
 
67
66
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
68
67
  if !@options.data_store.nil?
69
- get_id_lists_from_adapter(init_diagnostics)
68
+ get_id_lists_from_adapter
70
69
  else
71
- get_id_lists_from_network(init_diagnostics)
70
+ get_id_lists_from_network
72
71
  end
73
72
 
74
73
  @config_sync_thread = sync_config_specs
@@ -135,20 +134,20 @@ module Statsig
135
134
 
136
135
  private
137
136
 
138
- def load_config_specs_from_storage_adapter(init_diagnostics: nil)
139
- init_diagnostics&.mark("download_config_specs", "start", "fetch_from_adapter")
137
+ def load_config_specs_from_storage_adapter
138
+ tracker = @diagnostics.track('data_store_config_specs', 'fetch')
140
139
  cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
141
- init_diagnostics&.mark("download_config_specs", "end", "fetch_from_adapter", true)
140
+ tracker.end(true)
142
141
  return if cached_values.nil?
143
142
 
144
- init_diagnostics&.mark("download_config_specs", "start", "process")
143
+ tracker = @diagnostics.track('data_store_config_specs', 'process')
145
144
  process_specs(cached_values, from_adapter: true)
146
145
  @init_reason = EvaluationReason::DATA_ADAPTER
147
- init_diagnostics&.mark("download_config_specs", "end", "process", @init_reason)
146
+ tracker.end(true)
148
147
  rescue StandardError
149
148
  # Fallback to network
150
- init_diagnostics&.mark("download_config_specs", "end", "fetch_from_adapter", false)
151
- download_config_specs(init_diagnostics)
149
+ tracker.end(false)
150
+ download_config_specs
152
151
  end
153
152
 
154
153
  def save_config_specs_to_storage_adapter(specs_string)
@@ -160,6 +159,7 @@ module Statsig
160
159
 
161
160
  def sync_config_specs
162
161
  Thread.new do
162
+ @diagnostics = Diagnostics.new('config_sync')
163
163
  loop do
164
164
  sleep @options.rulesets_sync_interval
165
165
  if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
@@ -173,6 +173,7 @@ module Statsig
173
173
 
174
174
  def sync_id_lists
175
175
  Thread.new do
176
+ @diagnostics = Diagnostics.new('config_sync')
176
177
  loop do
177
178
  sleep @id_lists_sync_interval
178
179
  if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
@@ -184,8 +185,8 @@ module Statsig
184
185
  end
185
186
  end
186
187
 
187
- def download_config_specs(init_diagnostics = nil)
188
- init_diagnostics&.mark("download_config_specs", "start", "network_request")
188
+ def download_config_specs
189
+ tracker = @diagnostics.track('download_config_specs', 'network_request')
189
190
 
190
191
  error = nil
191
192
  begin
@@ -194,18 +195,17 @@ module Statsig
194
195
  if e.is_a? NetworkError
195
196
  code = e.http_code
196
197
  end
197
- init_diagnostics&.mark("download_config_specs", "end", "network_request", code)
198
+ tracker.end(code)
198
199
 
199
200
  if e.nil?
200
201
  unless response.nil?
201
- init_diagnostics&.mark("download_config_specs", "start", "process")
202
-
202
+ tracker = @diagnostics.track('download_config_specs', 'process')
203
203
  if process_specs(response.body.to_s)
204
204
  @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
205
  end
206
+ tracker.end(@init_reason == EvaluationReason::NETWORK)
207
207
 
208
- init_diagnostics&.mark("download_config_specs", "end", "process", @init_reason == EvaluationReason::NETWORK)
208
+ @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
209
209
  end
210
210
 
211
211
  nil
@@ -259,18 +259,18 @@ module Statsig
259
259
  true
260
260
  end
261
261
 
262
- def get_id_lists_from_adapter(init_diagnostics = nil)
263
- init_diagnostics&.mark("get_id_lists", "start", "fetch_from_adapter")
262
+ def get_id_lists_from_adapter
263
+ tracker = @diagnostics.track('data_store_id_lists', 'fetch')
264
264
  cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
265
265
  return if cached_values.nil?
266
266
 
267
- init_diagnostics&.mark("get_id_lists", "end", "fetch_from_adapter", true)
267
+ tracker.end(true)
268
268
  id_lists = JSON.parse(cached_values)
269
- process_id_lists(id_lists, init_diagnostics, from_adapter: true)
269
+ process_id_lists(id_lists, from_adapter: true)
270
270
  rescue StandardError
271
271
  # Fallback to network
272
- init_diagnostics&.mark("get_id_lists", "end", "fetch_from_adapter", false)
273
- get_id_lists_from_network(init_diagnostics)
272
+ tracker.end(false)
273
+ get_id_lists_from_network
274
274
  end
275
275
 
276
276
  def save_id_lists_to_adapter(id_lists_raw_json)
@@ -280,36 +280,45 @@ module Statsig
280
280
  @options.data_store.set(Interfaces::IDataStore::ID_LISTS_KEY, id_lists_raw_json)
281
281
  end
282
282
 
283
- def get_id_lists_from_network(init_diagnostics = nil)
284
- init_diagnostics&.mark("get_id_lists", "start", "network_request")
283
+ def get_id_lists_from_network
284
+ tracker = @diagnostics.track('get_id_list_sources', 'network_request')
285
285
  response, e = @network.post_helper('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
286
+ code = response&.status.to_i
287
+ if e.is_a? NetworkError
288
+ code = e.http_code
289
+ end
290
+ tracker.end(code)
286
291
  if !e.nil? || response.nil?
287
292
  return
288
293
  end
289
- init_diagnostics&.mark("get_id_lists", "end", "network_request", response.status.to_i)
290
294
 
291
295
  begin
292
296
  server_id_lists = JSON.parse(response)
293
- process_id_lists(server_id_lists, init_diagnostics)
297
+ process_id_lists(server_id_lists)
294
298
  save_id_lists_to_adapter(response.body.to_s)
295
299
  rescue
296
300
  # Ignored, will try again
297
301
  end
298
302
  end
299
303
 
300
- def process_id_lists(new_id_lists, init_diagnostics, from_adapter: false)
304
+ def process_id_lists(new_id_lists, from_adapter: false)
301
305
  local_id_lists = @specs[:id_lists]
302
306
  if !new_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
303
307
  return
304
308
  end
305
309
  tasks = []
306
310
 
307
- if new_id_lists.length == 0
311
+ tracker = @diagnostics.track(
312
+ from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
313
+ 'process',
314
+ new_id_lists.length
315
+ )
316
+
317
+ if new_id_lists.empty?
318
+ tracker.end
308
319
  return
309
320
  end
310
321
 
311
- init_diagnostics&.mark("get_id_lists", "start", "process", new_id_lists.length)
312
-
313
322
  delete_lists = []
314
323
  local_id_lists.each do |list_name, list|
315
324
  unless new_id_lists.key? list_name
@@ -357,14 +366,17 @@ module Statsig
357
366
  end
358
367
 
359
368
  result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
360
- init_diagnostics&.mark("get_id_lists", "end", "process", result.state == :fulfilled)
369
+ tracker.end(result.state == :fulfilled)
361
370
  end
362
371
 
363
372
  def get_single_id_list_from_adapter(list)
373
+ tracker = @diagnostics.track('data_store_id_list', 'fetch', nil, { url: list.url })
364
374
  cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
375
+ tracker.end(true)
365
376
  content = cached_values.to_s
366
- process_single_id_list(list, content)
377
+ process_single_id_list(list, content, from_adapter: true)
367
378
  rescue StandardError
379
+ tracker.end(false)
368
380
  nil
369
381
  end
370
382
 
@@ -378,7 +390,9 @@ module Statsig
378
390
  nil unless list.is_a? IDList
379
391
  http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
380
392
  begin
393
+ tracker = @diagnostics.track('get_id_list', 'network_request', nil, { url: list.url })
381
394
  res = http.get(list.url)
395
+ tracker.end(res.status.code)
382
396
  nil unless res.status.success?
383
397
  content_length = Integer(res['content-length'])
384
398
  nil if content_length.nil? || content_length <= 0
@@ -390,11 +404,13 @@ module Statsig
390
404
  end
391
405
  end
392
406
 
393
- def process_single_id_list(list, content, content_length = nil)
407
+ def process_single_id_list(list, content, content_length = nil, from_adapter: false)
394
408
  false unless list.is_a? IDList
395
409
  begin
410
+ tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', nil, { url: list.url })
396
411
  unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
397
412
  @specs[:id_lists].delete(list.name)
413
+ tracker.end(false)
398
414
  return false
399
415
  end
400
416
  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 +432,10 @@ module Statsig
416
432
  else
417
433
  list.size + content_length
418
434
  end
435
+ tracker.end(true)
419
436
  return true
420
437
  rescue
438
+ tracker.end(false)
421
439
  return false
422
440
  end
423
441
  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.25.1',
230
+ 'sdkVersion' => '1.25.2',
231
231
  }
232
232
  end
233
233
 
@@ -30,19 +30,20 @@ class StatsigDriver
30
30
  end
31
31
 
32
32
  @err_boundary = Statsig::ErrorBoundary.new(secret_key)
33
- @err_boundary.capture(-> {
34
- @init_diagnostics = Statsig::Diagnostics.new("initialize")
35
- @init_diagnostics.mark("overall", "start")
33
+ @err_boundary.capture(task: lambda {
34
+ @diagnostics = Statsig::Diagnostics.new('initialize')
35
+ tracker = @diagnostics.track('overall')
36
36
  @options = options || StatsigOptions.new
37
37
  @shutdown = false
38
38
  @secret_key = secret_key
39
39
  @net = Statsig::Network.new(secret_key, @options)
40
40
  @logger = Statsig::StatsigLogger.new(@net, @options)
41
- @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @init_diagnostics)
42
- @init_diagnostics.mark("overall", "end")
41
+ @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics)
42
+ tracker.end('success')
43
43
 
44
- log_init_diagnostics
44
+ @logger.log_diagnostics_event(@diagnostics)
45
45
  })
46
+ @err_boundary.logger = @logger
46
47
  end
47
48
 
48
49
  class CheckGateOptions < T::Struct
@@ -52,7 +53,7 @@ class StatsigDriver
52
53
  sig { params(user: StatsigUser, gate_name: String, options: CheckGateOptions).returns(T::Boolean) }
53
54
 
54
55
  def check_gate(user, gate_name, options = CheckGateOptions.new)
55
- @err_boundary.capture(-> {
56
+ @err_boundary.capture(task: lambda {
56
57
  user = verify_inputs(user, gate_name, "gate_name")
57
58
 
58
59
  res = @evaluator.check_gate(user, gate_name)
@@ -70,8 +71,7 @@ class StatsigDriver
70
71
  end
71
72
 
72
73
  res.gate_value
73
- }, -> { false })
74
-
74
+ }, recover: -> { false }, caller: __method__.to_s)
75
75
  end
76
76
 
77
77
  sig { params(user: StatsigUser, gate_name: String).void }
@@ -89,10 +89,10 @@ class StatsigDriver
89
89
  sig { params(user: StatsigUser, dynamic_config_name: String, options: GetConfigOptions).returns(DynamicConfig) }
90
90
 
91
91
  def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
92
- @err_boundary.capture(-> {
92
+ @err_boundary.capture(task: lambda {
93
93
  user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
94
94
  get_config_impl(user, dynamic_config_name, options)
95
- }, -> { DynamicConfig.new(dynamic_config_name) })
95
+ }, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
96
96
  end
97
97
 
98
98
  class GetExperimentOptions < T::Struct
@@ -102,10 +102,10 @@ class StatsigDriver
102
102
  sig { params(user: StatsigUser, experiment_name: String, options: GetExperimentOptions).returns(DynamicConfig) }
103
103
 
104
104
  def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
105
- @err_boundary.capture(-> {
105
+ @err_boundary.capture(task: lambda {
106
106
  user = verify_inputs(user, experiment_name, "experiment_name")
107
107
  get_config_impl(user, experiment_name, options)
108
- }, -> { DynamicConfig.new(experiment_name) })
108
+ }, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
109
109
  end
110
110
 
111
111
  sig { params(user: StatsigUser, config_name: String).void }
@@ -123,7 +123,7 @@ class StatsigDriver
123
123
  sig { params(user: StatsigUser, layer_name: String, options: GetLayerOptions).returns(Layer) }
124
124
 
125
125
  def get_layer(user, layer_name, options = GetLayerOptions.new)
126
- @err_boundary.capture(-> {
126
+ @err_boundary.capture(task: lambda {
127
127
  user = verify_inputs(user, layer_name, "layer_name")
128
128
 
129
129
  res = @evaluator.get_layer(user, layer_name)
@@ -143,9 +143,9 @@ class StatsigDriver
143
143
  @logger.log_layer_exposure(user, layer, parameter_name, res)
144
144
  } : nil
145
145
  Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
146
- }, -> {
146
+ }, recover: lambda {
147
147
  Layer.new(layer_name)
148
- })
148
+ }, caller: __method__.to_s)
149
149
  end
150
150
 
151
151
  sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
@@ -158,7 +158,7 @@ class StatsigDriver
158
158
  end
159
159
 
160
160
  def log_event(user, event_name, value = nil, metadata = nil)
161
- @err_boundary.capture(-> {
161
+ @err_boundary.capture(task: lambda {
162
162
  if !user.nil? && !user.instance_of?(StatsigUser)
163
163
  raise Statsig::ValueError.new('Must provide a valid StatsigUser or nil')
164
164
  end
@@ -175,7 +175,7 @@ class StatsigDriver
175
175
  end
176
176
 
177
177
  def shutdown
178
- @err_boundary.capture(-> {
178
+ @err_boundary.capture(task: lambda {
179
179
  @shutdown = true
180
180
  @logger.shutdown
181
181
  @evaluator.shutdown
@@ -183,13 +183,13 @@ class StatsigDriver
183
183
  end
184
184
 
185
185
  def override_gate(gate_name, gate_value)
186
- @err_boundary.capture(-> {
186
+ @err_boundary.capture(task: lambda {
187
187
  @evaluator.override_gate(gate_name, gate_value)
188
188
  })
189
189
  end
190
190
 
191
191
  def override_config(config_name, config_value)
192
- @err_boundary.capture(-> {
192
+ @err_boundary.capture(task: lambda {
193
193
  @evaluator.override_config(config_name, config_value)
194
194
  })
195
195
  end
@@ -197,11 +197,11 @@ class StatsigDriver
197
197
  # @param [StatsigUser] user
198
198
  # @return [Hash]
199
199
  def get_client_initialize_response(user)
200
- @err_boundary.capture(-> {
200
+ @err_boundary.capture(task: lambda {
201
201
  validate_user(user)
202
202
  normalize_user(user)
203
203
  @evaluator.get_client_initialize_response(user)
204
- }, -> { nil })
204
+ }, recover: -> { nil })
205
205
  end
206
206
 
207
207
  def maybe_restart_background_threads
@@ -209,7 +209,7 @@ class StatsigDriver
209
209
  return
210
210
  end
211
211
 
212
- @err_boundary.capture(-> {
212
+ @err_boundary.capture(task: lambda {
213
213
  @evaluator.maybe_restart_background_threads
214
214
  @logger.maybe_restart_background_threads
215
215
  })
@@ -302,12 +302,4 @@ class StatsigDriver
302
302
  network_result['rule_id'],
303
303
  )
304
304
  end
305
-
306
- def log_init_diagnostics
307
- if @options.disable_diagnostics_logging
308
- return
309
- end
310
-
311
- @logger.log_diagnostics_event(@init_diagnostics)
312
- end
313
305
  end
@@ -97,10 +97,14 @@ module Statsig
97
97
  end
98
98
 
99
99
  def log_diagnostics_event(diagnostics, user = nil)
100
+ return if @options.disable_diagnostics_logging
101
+ return if diagnostics.nil? || diagnostics.markers.empty?
102
+
100
103
  event = StatsigEvent.new($diagnostics_event)
101
104
  event.user = user
102
105
  event.metadata = diagnostics.serialize
103
106
  log_event(event)
107
+ diagnostics.clear_markers
104
108
  end
105
109
 
106
110
  def periodic_flush
@@ -19,6 +19,11 @@ class StatsigOptions
19
19
  # default: https://statsigapi.net/v1
20
20
  attr_accessor :api_url_base
21
21
 
22
+ # The base url used specifically to call download_config_specs.
23
+ # Takes precedence over api_url_base
24
+ sig { returns(T.any(String, NilClass)) }
25
+ attr_accessor :api_url_download_config_specs
26
+
22
27
  sig { returns(T.any(Float, Integer)) }
23
28
  # The interval (in seconds) to poll for changes to your Statsig configuration
24
29
  # default: 10s
@@ -97,6 +102,7 @@ class StatsigOptions
97
102
  params(
98
103
  environment: T.any(T::Hash[String, String], NilClass),
99
104
  api_url_base: String,
105
+ api_url_download_config_specs: T.any(String, NilClass),
100
106
  rulesets_sync_interval: T.any(Float, Integer),
101
107
  idlists_sync_interval: T.any(Float, Integer),
102
108
  logging_interval_seconds: T.any(Float, Integer),
@@ -118,6 +124,7 @@ class StatsigOptions
118
124
  def initialize(
119
125
  environment = nil,
120
126
  api_url_base = 'https://statsigapi.net/v1',
127
+ api_url_download_config_specs: nil,
121
128
  rulesets_sync_interval: 10,
122
129
  idlists_sync_interval: 60,
123
130
  logging_interval_seconds: 60,
@@ -135,6 +142,7 @@ class StatsigOptions
135
142
  post_logs_retry_backoff: nil)
136
143
  @environment = environment.is_a?(Hash) ? environment : nil
137
144
  @api_url_base = api_url_base
145
+ @api_url_download_config_specs = api_url_download_config_specs
138
146
  @rulesets_sync_interval = rulesets_sync_interval
139
147
  @idlists_sync_interval = idlists_sync_interval
140
148
  @logging_interval_seconds = logging_interval_seconds
data/lib/uri_helper.rb ADDED
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ class URIHelper
6
+ class URIBuilder
7
+ extend T::Sig
8
+
9
+ sig { returns(StatsigOptions) }
10
+ attr_accessor :options
11
+
12
+ sig { params(options: StatsigOptions).void }
13
+ def initialize(options)
14
+ @options = options
15
+ end
16
+
17
+ sig { params(endpoint: String).returns(String) }
18
+ def build_url(endpoint)
19
+ api = @options.api_url_base
20
+ if endpoint == 'download_config_specs' && !@options.api_url_download_config_specs.nil?
21
+ api = T.must(@options.api_url_download_config_specs)
22
+ end
23
+ unless api.end_with?('/')
24
+ api += '/'
25
+ end
26
+ "#{api}#{endpoint}"
27
+ end
28
+ end
29
+
30
+ def self.initialize(options)
31
+ @uri_builder = URIBuilder.new(options)
32
+ end
33
+
34
+ def self.build_url(endpoint)
35
+ @uri_builder.build_url(endpoint)
36
+ end
37
+ 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.25.1
4
+ version: 1.25.2
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-05-31 00:00:00.000000000 Z
11
+ date: 2023-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -295,6 +295,7 @@ files:
295
295
  - lib/statsig_options.rb
296
296
  - lib/statsig_user.rb
297
297
  - lib/ua_parser.rb
298
+ - lib/uri_helper.rb
298
299
  homepage: https://rubygems.org/gems/statsig
299
300
  licenses:
300
301
  - ISC