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.
- checksums.yaml +4 -4
- data/lib/api_config.rb +0 -141
- data/lib/client_initialize_helpers.rb +26 -27
- data/lib/constants.rb +65 -0
- data/lib/dynamic_config.rb +13 -5
- data/lib/evaluation_helpers.rb +16 -5
- data/lib/evaluator.rb +146 -116
- data/lib/interfaces/data_store.rb +1 -1
- data/lib/layer.rb +12 -5
- data/lib/memo.rb +8 -6
- data/lib/network.rb +41 -28
- data/lib/spec_store.rb +39 -47
- data/lib/statsig.rb +1 -1
- data/lib/statsig_driver.rb +11 -6
- data/lib/statsig_options.rb +19 -10
- metadata +16 -3
- data/lib/uri_helper.rb +0 -29
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
|
-
|
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
|
-
|
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(
|
59
|
-
request(:GET,
|
82
|
+
def get(url, retries = 0, backoff = 1)
|
83
|
+
request(:GET, url, nil, retries, backoff)
|
60
84
|
end
|
61
85
|
|
62
|
-
def post(
|
63
|
-
request(:POST,
|
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,
|
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
|
-
|
102
|
+
|
79
103
|
begin
|
80
104
|
res = @connection_pool.with do |conn|
|
81
|
-
request = conn.headers(
|
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,
|
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,
|
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
|
-
|
118
|
-
|
119
|
-
@gates[
|
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
|
-
|
124
|
+
config_sym = config_name.to_sym
|
125
|
+
return nil unless has_config?(config_sym)
|
124
126
|
|
125
|
-
@configs[
|
127
|
+
@configs[config_sym]
|
126
128
|
end
|
127
129
|
|
128
130
|
def get_layer(layer_name)
|
129
|
-
|
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
|
-
|
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::
|
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::
|
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
|
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::
|
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
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
@
|
303
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
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.
|
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
data/lib/statsig_driver.rb
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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,
|
data/lib/statsig_options.rb
CHANGED
@@ -10,13 +10,14 @@ class StatsigOptions
|
|
10
10
|
# eg. { "tier" => "development" }
|
11
11
|
attr_accessor :environment
|
12
12
|
|
13
|
-
# The
|
14
|
-
|
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
|
18
|
-
|
19
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
115
|
-
|
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:
|
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-
|
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
|