statsig 1.34.1 → 2.0.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/network.rb CHANGED
@@ -3,7 +3,6 @@ require 'json'
3
3
  require 'securerandom'
4
4
  require 'zlib'
5
5
 
6
- require 'uri_helper'
7
6
  require 'connection_pool'
8
7
 
9
8
  RETRY_CODES = [408, 500, 502, 503, 504, 522, 524, 599].freeze
@@ -22,7 +21,7 @@ module Statsig
22
21
 
23
22
  def initialize(server_secret, options, backoff_mult = 10)
24
23
  super()
25
- URIHelper.initialize(options)
24
+ @options = options
26
25
  @server_secret = server_secret
27
26
  @local_mode = options.local_mode
28
27
  @timeout = options.network_timeout
@@ -43,6 +42,7 @@ module Statsig
43
42
  'Accept-Encoding' => 'gzip'
44
43
  }
45
44
  ).accept(:json)
45
+
46
46
  if @timeout
47
47
  client = client.timeout(@timeout)
48
48
  end
@@ -52,18 +52,42 @@ module Statsig
52
52
  end
53
53
 
54
54
  def download_config_specs(since_time)
55
- get("download_config_specs/#{@server_secret}.json?sinceTime=#{since_time}")
55
+ url = @options.download_config_specs_url
56
+ get("#{url}#{@server_secret}.json?sinceTime=#{since_time}")
57
+ end
58
+
59
+ def post_logs(events, error_boundary)
60
+ url = @options.log_event_url
61
+ event_count = events.length
62
+ json_body = JSON.generate({ events: events, statsigMetadata: Statsig.get_statsig_metadata })
63
+ gzip = Zlib::GzipWriter.new(StringIO.new)
64
+ gzip << json_body
65
+
66
+ response, e = post(url, gzip.close.string, @post_logs_retry_limit, 1, true, event_count)
67
+ unless e == nil
68
+ message = "Failed to log #{event_count} events after #{@post_logs_retry_limit} retries"
69
+ puts "[Statsig]: #{message}"
70
+ error_boundary.log_exception(e, tag: 'statsig::log_event_failed', extra: { eventCount: event_count, error: message }, force: true)
71
+ return
72
+ end
73
+ rescue StandardError
74
+
75
+ end
76
+
77
+ def get_id_lists
78
+ url = @options.get_id_lists_url
79
+ post(url, JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
56
80
  end
57
81
 
58
- def get(endpoint, retries = 0, backoff = 1)
59
- request(:GET, endpoint, nil, retries, backoff)
82
+ def get(url, retries = 0, backoff = 1)
83
+ request(:GET, url, nil, retries, backoff)
60
84
  end
61
85
 
62
- def post(endpoint, body, retries = 0, backoff = 1, zipped = false, event_count = 0)
63
- request(:POST, endpoint, body, retries, backoff, zipped, event_count)
86
+ def post(url, body, retries = 0, backoff = 1, zipped = false, event_count = 0)
87
+ request(:POST, url, body, retries, backoff, zipped, event_count)
64
88
  end
65
89
 
66
- def request(method, endpoint, body, retries = 0, backoff = 1, zipped = false, event_count = 0)
90
+ def request(method, url, body, retries = 0, backoff = 1, zipped = false, event_count = 0)
67
91
  if @local_mode
68
92
  return nil, nil
69
93
  end
@@ -75,10 +99,15 @@ module Statsig
75
99
  backoff_adjusted = @post_logs_retry_backoff.call(retries)
76
100
  end
77
101
  end
78
- url = URIHelper.build_url(endpoint)
102
+
79
103
  begin
80
104
  res = @connection_pool.with do |conn|
81
- request = conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s, 'CONTENT-ENCODING' => zipped ? 'gzip' : nil, 'STATSIG-EVENT-COUNT' => event_count == 0 ? nil : event_count.to_s)
105
+ request = conn.headers(
106
+ 'STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s,
107
+ 'CONTENT-ENCODING' => zipped ? 'gzip' : nil,
108
+ 'STATSIG-EVENT-COUNT' => event_count == 0 ? nil : event_count.to_s
109
+ )
110
+
82
111
  case method
83
112
  when :GET
84
113
  request.get(url)
@@ -91,7 +120,7 @@ module Statsig
91
120
  return nil, e unless retries.positive?
92
121
 
93
122
  sleep backoff_adjusted
94
- return request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count)
123
+ return request(method, url, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count)
95
124
  end
96
125
  return res, nil if res.status.success?
97
126
 
@@ -102,23 +131,7 @@ module Statsig
102
131
 
103
132
  ## status code retry
104
133
  sleep backoff_adjusted
105
- request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count)
106
- end
107
-
108
- def post_logs(events, error_boundary)
109
- event_count = events.length
110
- json_body = JSON.generate({ events: events, statsigMetadata: Statsig.get_statsig_metadata })
111
- gzip = Zlib::GzipWriter.new(StringIO.new)
112
- gzip << json_body
113
- response, e = post('log_event', gzip.close.string, @post_logs_retry_limit, 1, true, event_count)
114
- unless e == nil
115
- message = "Failed to log #{event_count} events after #{@post_logs_retry_limit} retries"
116
- puts "[Statsig]: #{message}"
117
- error_boundary.log_exception(e, tag: 'statsig::log_event_failed', extra: { eventCount: event_count, error: message }, force: true)
118
- return
119
- end
120
- rescue StandardError
121
-
134
+ request(method, url, body, retries - 1, backoff * @backoff_multiplier, zipped, event_count)
122
135
  end
123
136
  end
124
137
  end
data/lib/spec_store.rb CHANGED
@@ -33,6 +33,7 @@ module Statsig
33
33
  @gates = {}
34
34
  @configs = {}
35
35
  @layers = {}
36
+ @condition_map = {}
36
37
  @id_lists = {}
37
38
  @experiment_to_layer = {}
38
39
  @sdk_keys_to_app_ids = {}
@@ -102,33 +103,39 @@ module Statsig
102
103
  end
103
104
 
104
105
  def has_gate?(gate_name)
105
- @gates.key?(gate_name)
106
+ @gates.key?(gate_name.to_sym)
106
107
  end
107
108
 
108
109
  def has_config?(config_name)
109
- @configs.key?(config_name)
110
+ @configs.key?(config_name.to_sym)
110
111
  end
111
112
 
112
113
  def has_layer?(layer_name)
113
- @layers.key?(layer_name)
114
+ @layers.key?(layer_name.to_sym)
114
115
  end
115
116
 
116
117
  def get_gate(gate_name)
117
- return nil unless has_gate?(gate_name)
118
-
119
- @gates[gate_name]
118
+ gate_sym = gate_name.to_sym
119
+ return nil unless has_gate?(gate_sym)
120
+ @gates[gate_sym]
120
121
  end
121
122
 
122
123
  def get_config(config_name)
123
- return nil unless has_config?(config_name)
124
+ config_sym = config_name.to_sym
125
+ return nil unless has_config?(config_sym)
124
126
 
125
- @configs[config_name]
127
+ @configs[config_sym]
126
128
  end
127
129
 
128
130
  def get_layer(layer_name)
129
- return nil unless has_layer?(layer_name)
131
+ layer_sym = layer_name.to_sym
132
+ return nil unless has_layer?(layer_sym)
133
+
134
+ @layers[layer_sym]
135
+ end
130
136
 
131
- @layers[layer_name]
137
+ def get_condition(condition_hash)
138
+ @condition_map[condition_hash.to_sym]
132
139
  end
133
140
 
134
141
  def get_id_list(list_name)
@@ -169,7 +176,7 @@ module Statsig
169
176
  end
170
177
 
171
178
  def sync_config_specs
172
- if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
179
+ if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_V2_KEY)
173
180
  load_config_specs_from_storage_adapter('config_sync')
174
181
  else
175
182
  download_config_specs('config_sync')
@@ -190,7 +197,7 @@ module Statsig
190
197
 
191
198
  def load_config_specs_from_storage_adapter(context)
192
199
  tracker = @diagnostics.track(context, 'data_store_config_specs', 'fetch')
193
- cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
200
+ cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_V2_KEY)
194
201
  tracker.end(success: true)
195
202
  return if cached_values.nil?
196
203
 
@@ -204,12 +211,12 @@ module Statsig
204
211
  download_config_specs(context)
205
212
  end
206
213
 
207
- def save_config_specs_to_storage_adapter(specs_string)
214
+ def save_rulesets_to_storage_adapter(rulesets_string)
208
215
  if @options.data_store.nil?
209
216
  return
210
217
  end
211
218
 
212
- @options.data_store.set(Interfaces::IDataStore::CONFIG_SPECS_KEY, specs_string)
219
+ @options.data_store.set(Interfaces::IDataStore::CONFIG_SPECS_V2_KEY, rulesets_string)
213
220
  end
214
221
 
215
222
  def spawn_sync_config_specs_thread
@@ -293,50 +300,35 @@ module Statsig
293
300
  return false
294
301
  end
295
302
 
296
- @last_config_sync_time = specs_json[:time] || @last_config_sync_time
297
- return false unless specs_json[:has_updates] == true &&
298
- !specs_json[:feature_gates].nil? &&
299
- !specs_json[:dynamic_configs].nil? &&
300
- !specs_json[:layer_configs].nil?
303
+ new_specs_sync_time = specs_json[:time]
304
+ if new_specs_sync_time.nil? \
305
+ || new_specs_sync_time < @last_config_sync_time \
306
+ || specs_json[:has_updates] != true \
307
+ || specs_json[:feature_gates].nil? \
308
+ || specs_json[:dynamic_configs].nil? \
309
+ || specs_json[:layer_configs].nil?
310
+ return false
311
+ end
301
312
 
302
- @unsupported_configs.clear()
303
- new_gates = process_configs(specs_json[:feature_gates])
304
- new_configs = process_configs(specs_json[:dynamic_configs])
305
- new_layers = process_configs(specs_json[:layer_configs])
313
+ @last_config_sync_time = new_specs_sync_time
314
+ @unsupported_configs.clear
306
315
 
307
- new_exp_to_layer = {}
308
316
  specs_json[:diagnostics]&.each { |key, value| @diagnostics.sample_rates[key.to_s] = value }
309
317
 
310
- if specs_json[:layers].is_a?(Hash)
311
- specs_json[:layers].each do |layer_name, experiments|
312
- experiments.each { |experiment_name| new_exp_to_layer[experiment_name] = layer_name }
313
- end
314
- end
315
-
316
- @gates = new_gates
317
- @configs = new_configs
318
- @layers = new_layers
319
- @experiment_to_layer = new_exp_to_layer
318
+ @gates = specs_json[:feature_gates]
319
+ @configs = specs_json[:dynamic_configs]
320
+ @layers = specs_json[:layer_configs]
321
+ @condition_map = specs_json[:condition_map]
322
+ @experiment_to_layer = specs_json[:experiment_to_layer]
320
323
  @sdk_keys_to_app_ids = specs_json[:sdk_keys_to_app_ids] || {}
321
324
  @hashed_sdk_keys_to_app_ids = specs_json[:hashed_sdk_keys_to_app_ids] || {}
322
325
 
323
326
  unless from_adapter
324
- save_config_specs_to_storage_adapter(specs_string)
327
+ save_rulesets_to_storage_adapter(specs_string)
325
328
  end
326
329
  true
327
330
  end
328
331
 
329
- def process_configs(configs)
330
- configs.each_with_object({}) do |config, new_configs|
331
- begin
332
- new_configs[config[:name]] = APIConfig.from_json(config)
333
- rescue UnsupportedConfigException => e
334
- @unsupported_configs.add(config[:name])
335
- nil
336
- end
337
- end
338
- end
339
-
340
332
  def get_id_lists_from_adapter(context)
341
333
  tracker = @diagnostics.track(context, 'data_store_id_lists', 'fetch')
342
334
  cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
@@ -361,7 +353,7 @@ module Statsig
361
353
 
362
354
  def get_id_lists_from_network(context)
363
355
  tracker = @diagnostics.track(context, 'get_id_list_sources', 'network_request')
364
- response, e = @network.post('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
356
+ response, e = @network.get_id_lists
365
357
  code = response&.status.to_i
366
358
  if e.is_a? NetworkError
367
359
  code = e.http_code
data/lib/statsig.rb CHANGED
@@ -363,7 +363,7 @@ module Statsig
363
363
  def self.get_statsig_metadata
364
364
  {
365
365
  'sdkType' => 'ruby-server',
366
- 'sdkVersion' => '1.34.1',
366
+ 'sdkVersion' => '2.0.0',
367
367
  'languageVersion' => RUBY_VERSION
368
368
  }
369
369
  end
@@ -54,20 +54,25 @@ class StatsigDriver
54
54
  if skip_evaluation
55
55
  gate = @store.get_gate(gate_name)
56
56
  return FeatureGate.new(gate_name) if gate.nil?
57
- return FeatureGate.new(gate.name, target_app_ids: gate.target_app_ids)
57
+ return FeatureGate.new(gate_name, target_app_ids: gate[:targetAppIDs])
58
58
  end
59
59
 
60
60
  user = verify_inputs(user, gate_name, 'gate_name')
61
- return Statsig::Memo.for(user.get_memo(), :get_gate_impl, gate_name) do
62
61
 
63
- res = Statsig::ConfigResult.new(name: gate_name, disable_exposures: disable_log_exposure, disable_evaluation_details: disable_evaluation_details)
62
+ Statsig::Memo.for(user.get_memo, :get_gate_impl, gate_name) do
63
+ res = Statsig::ConfigResult.new(
64
+ name: gate_name,
65
+ disable_exposures: disable_log_exposure,
66
+ disable_evaluation_details: disable_evaluation_details
67
+ )
64
68
  @evaluator.check_gate(user, gate_name, res, ignore_local_overrides: ignore_local_overrides)
65
69
 
66
70
  unless disable_log_exposure
67
- @logger.log_gate_exposure(
71
+ @logger.log_gate_exposure(
68
72
  user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
69
73
  )
70
74
  end
75
+
71
76
  FeatureGate.from_config_result(res)
72
77
  end
73
78
  end
@@ -162,7 +167,7 @@ class StatsigDriver
162
167
  @err_boundary.capture(caller: __method__, recover: -> { Layer.new(layer_name) }) do
163
168
  run_with_diagnostics(caller: :get_layer) do
164
169
  user = verify_inputs(user, layer_name, "layer_name")
165
- Statsig::Memo.for(user.get_memo(), :get_layer, layer_name) do
170
+ Statsig::Memo.for(user.get_memo, :get_layer, layer_name) do
166
171
  exposures_disabled = options&.disable_log_exposure == true
167
172
  res = Statsig::ConfigResult.new(
168
173
  name: layer_name,
@@ -367,7 +372,7 @@ class StatsigDriver
367
372
  end
368
373
 
369
374
  def get_config_impl(user, config_name, disable_log_exposure, user_persisted_values: nil, disable_evaluation_details: false, ignore_local_overrides: false)
370
- return Statsig::Memo.for(user.get_memo(), :get_config_impl, config_name) do
375
+ Statsig::Memo.for(user.get_memo, :get_config_impl, config_name) do
371
376
  res = Statsig::ConfigResult.new(
372
377
  name: config_name,
373
378
  disable_exposures: disable_log_exposure,
@@ -10,13 +10,14 @@ class StatsigOptions
10
10
  # eg. { "tier" => "development" }
11
11
  attr_accessor :environment
12
12
 
13
- # The base url used to make network calls to Statsig.
14
- # default: https://statsigapi.net/v1
15
- attr_accessor :api_url_base
13
+ # The url used specifically to call download_config_specs.
14
+ attr_accessor :download_config_specs_url
16
15
 
17
- # The base url used specifically to call download_config_specs.
18
- # Takes precedence over api_url_base
19
- attr_accessor :api_url_download_config_specs
16
+ # The url used specifically to call log_event.
17
+ attr_accessor :log_event_url
18
+
19
+ # The url used specifically to call get_id_lists.
20
+ attr_accessor :get_id_lists_url
20
21
 
21
22
  # The interval (in seconds) to poll for changes to your Statsig configuration
22
23
  # default: 10s
@@ -89,8 +90,9 @@ class StatsigOptions
89
90
 
90
91
  def initialize(
91
92
  environment = nil,
92
- api_url_base = nil,
93
- api_url_download_config_specs: nil,
93
+ download_config_specs_url: nil,
94
+ log_event_url: nil,
95
+ get_id_lists_url: nil,
94
96
  rulesets_sync_interval: 10,
95
97
  idlists_sync_interval: 60,
96
98
  disable_rulesets_sync: false,
@@ -111,8 +113,15 @@ class StatsigOptions
111
113
  user_persistent_storage: nil
112
114
  )
113
115
  @environment = environment.is_a?(Hash) ? environment : nil
114
- @api_url_base = api_url_base || 'https://statsigapi.net/v1'
115
- @api_url_download_config_specs = api_url_download_config_specs || api_url_base || 'https://api.statsigcdn.com/v1'
116
+
117
+ dcs_url = download_config_specs_url || 'https://api.statsigcdn.com/v2/download_config_specs/'
118
+ unless dcs_url.end_with?('/')
119
+ dcs_url += '/'
120
+ end
121
+ @download_config_specs_url = dcs_url
122
+
123
+ @log_event_url = log_event_url || 'https://statsigapi.net/v1/log_event'
124
+ @get_id_lists_url = get_id_lists_url || 'https://statsigapi.net/v1/get_id_lists'
116
125
  @rulesets_sync_interval = rulesets_sync_interval
117
126
  @idlists_sync_interval = idlists_sync_interval
118
127
  @disable_rulesets_sync = disable_rulesets_sync
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.34.1
4
+ version: 2.0.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: 2024-06-12 00:00:00.000000000 Z
11
+ date: 2024-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: mutex_m
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.2.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.2.0
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: tapioca
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -335,7 +349,6 @@ files:
335
349
  - lib/statsig_options.rb
336
350
  - lib/statsig_user.rb
337
351
  - lib/ua_parser.rb
338
- - lib/uri_helper.rb
339
352
  - lib/user_persistent_storage_utils.rb
340
353
  homepage: https://rubygems.org/gems/statsig
341
354
  licenses:
data/lib/uri_helper.rb DELETED
@@ -1,29 +0,0 @@
1
- class URIHelper
2
- class URIBuilder
3
-
4
- attr_accessor :options
5
-
6
- def initialize(options)
7
- @options = options
8
- end
9
-
10
- def build_url(endpoint)
11
- api = @options.api_url_base
12
- if endpoint.include?('download_config_specs')
13
- api = @options.api_url_download_config_specs
14
- end
15
- unless api.end_with?('/')
16
- api += '/'
17
- end
18
- "#{api}#{endpoint}"
19
- end
20
- end
21
-
22
- def self.initialize(options)
23
- @uri_builder = URIBuilder.new(options)
24
- end
25
-
26
- def self.build_url(endpoint)
27
- @uri_builder.build_url(endpoint)
28
- end
29
- end