statsig 1.34.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/api_config.rb +0 -141
- data/lib/client_initialize_helpers.rb +21 -20
- 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
|