statsig 1.34.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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.2',
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.2
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-24 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