statsig 1.28.0 → 1.29.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/client_initialize_helpers.rb +1 -1
- data/lib/config_result.rb +41 -2
- data/lib/dynamic_config.rb +15 -2
- data/lib/evaluation_details.rb +12 -7
- data/lib/evaluator.rb +54 -16
- data/lib/id_list.rb +1 -1
- data/lib/interfaces/user_persistent_storage.rb +12 -0
- data/lib/layer.rb +17 -3
- data/lib/network.rb +44 -7
- data/lib/spec_store.rb +14 -3
- data/lib/statsig.rb +43 -18
- data/lib/statsig_driver.rb +37 -43
- data/lib/statsig_options.rb +19 -9
- data/lib/statsig_user.rb +10 -4
- data/lib/uri_helper.rb +1 -1
- data/lib/user_persistent_storage_utils.rb +106 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '093cc05e0b8f3bb7e2f0f019cf36ad33e5abc4c4349d2fb1faa056c1ff5aa341'
|
4
|
+
data.tar.gz: d66640f8b0c317ca018ff4ccc77d1d370af89b1c84416508631deb610236d519
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f634575320cd1ababfa25816ee8ac5fe27e34c761895b4ba6b20957363e487c295767d65e123ab7d306aa1ce2f535f55cc58570f8767790948d2647ab321150b
|
7
|
+
data.tar.gz: d563727b175a23afa50cb68cbaf91eb49435bae46b1a48b72d9bf0ff38febeefa86b886c34cc2632f37e357c44f96a1e2586050b41e9ac156697f4105c4f9f0b
|
@@ -42,7 +42,7 @@ module ClientInitializeHelpers
|
|
42
42
|
target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
|
43
43
|
config_target_apps = config_spec['targetAppIDs']
|
44
44
|
|
45
|
-
unless target_app_id.nil? || config_target_apps.nil?
|
45
|
+
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
46
46
|
return nil
|
47
47
|
end
|
48
48
|
|
data/lib/config_result.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
# typed: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
2
5
|
module Statsig
|
3
6
|
class ConfigResult
|
7
|
+
extend T::Sig
|
8
|
+
|
4
9
|
attr_accessor :name
|
5
10
|
attr_accessor :gate_value
|
6
11
|
attr_accessor :json_value
|
@@ -20,7 +25,7 @@ module Statsig
|
|
20
25
|
json_value = {},
|
21
26
|
rule_id = '',
|
22
27
|
secondary_exposures = [],
|
23
|
-
config_delegate =
|
28
|
+
config_delegate = nil,
|
24
29
|
explicit_parameters = [],
|
25
30
|
is_experiment_group: false,
|
26
31
|
evaluation_details: nil,
|
@@ -39,5 +44,39 @@ module Statsig
|
|
39
44
|
@group_name = group_name
|
40
45
|
@id_type = id_type
|
41
46
|
end
|
47
|
+
|
48
|
+
sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
|
49
|
+
def self.from_user_persisted_values(config_name, user_persisted_values)
|
50
|
+
sticky_values = user_persisted_values[config_name]
|
51
|
+
return nil if sticky_values.nil?
|
52
|
+
|
53
|
+
from_hash(config_name, sticky_values)
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { params(config_name: String, hash: Hash).returns(ConfigResult) }
|
57
|
+
def self.from_hash(config_name, hash)
|
58
|
+
new(
|
59
|
+
config_name,
|
60
|
+
hash['gate_value'],
|
61
|
+
hash['json_value'],
|
62
|
+
hash['rule_id'],
|
63
|
+
hash['secondary_exposures'],
|
64
|
+
evaluation_details: EvaluationDetails.persisted(hash['config_sync_time'], hash['init_time']),
|
65
|
+
group_name: hash['group_name']
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { returns(Hash) }
|
70
|
+
def to_hash
|
71
|
+
{
|
72
|
+
json_value: @json_value,
|
73
|
+
gate_value: @gate_value,
|
74
|
+
rule_id: @rule_id,
|
75
|
+
secondary_exposures: @secondary_exposures,
|
76
|
+
config_sync_time: @evaluation_details.config_sync_time,
|
77
|
+
init_time: @init_time,
|
78
|
+
group_name: @group_name
|
79
|
+
}
|
80
|
+
end
|
42
81
|
end
|
43
|
-
end
|
82
|
+
end
|
data/lib/dynamic_config.rb
CHANGED
@@ -26,13 +26,26 @@ class DynamicConfig
|
|
26
26
|
sig { returns(String) }
|
27
27
|
attr_accessor :id_type
|
28
28
|
|
29
|
-
sig {
|
30
|
-
|
29
|
+
sig { returns(T.nilable(Statsig::EvaluationDetails)) }
|
30
|
+
attr_accessor :evaluation_details
|
31
|
+
|
32
|
+
sig do
|
33
|
+
params(
|
34
|
+
name: String,
|
35
|
+
value: T::Hash[String, T.untyped],
|
36
|
+
rule_id: String,
|
37
|
+
group_name: T.nilable(String),
|
38
|
+
id_type: String,
|
39
|
+
evaluation_details: T.nilable(Statsig::EvaluationDetails)
|
40
|
+
).void
|
41
|
+
end
|
42
|
+
def initialize(name, value = {}, rule_id = '', group_name = nil, id_type = '', evaluation_details = nil)
|
31
43
|
@name = name
|
32
44
|
@value = value
|
33
45
|
@rule_id = rule_id
|
34
46
|
@group_name = group_name
|
35
47
|
@id_type = id_type
|
48
|
+
@evaluation_details = evaluation_details
|
36
49
|
end
|
37
50
|
|
38
51
|
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
data/lib/evaluation_details.rb
CHANGED
@@ -2,12 +2,13 @@
|
|
2
2
|
module Statsig
|
3
3
|
|
4
4
|
module EvaluationReason
|
5
|
-
NETWORK =
|
6
|
-
LOCAL_OVERRIDE =
|
7
|
-
UNRECOGNIZED =
|
8
|
-
UNINITIALIZED =
|
9
|
-
BOOTSTRAP =
|
10
|
-
DATA_ADAPTER =
|
5
|
+
NETWORK = 'Network'.freeze
|
6
|
+
LOCAL_OVERRIDE = 'LocalOverride'.freeze
|
7
|
+
UNRECOGNIZED = 'Unrecognized'.freeze
|
8
|
+
UNINITIALIZED = 'Uninitialized'.freeze
|
9
|
+
BOOTSTRAP = 'Bootstrap'.freeze
|
10
|
+
DATA_ADAPTER = 'DataAdapter'.freeze
|
11
|
+
PERSISTED = 'Persisted'.freeze
|
11
12
|
end
|
12
13
|
|
13
14
|
class EvaluationDetails
|
@@ -38,5 +39,9 @@ module Statsig
|
|
38
39
|
def self.local_override(config_sync_time, init_time)
|
39
40
|
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::LOCAL_OVERRIDE)
|
40
41
|
end
|
42
|
+
|
43
|
+
def self.persisted(config_sync_time, init_time)
|
44
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::PERSISTED)
|
45
|
+
end
|
41
46
|
end
|
42
|
-
end
|
47
|
+
end
|
data/lib/evaluator.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# typed: false
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
2
4
|
require 'config_result'
|
3
5
|
require 'country_lookup'
|
4
6
|
require 'digest'
|
@@ -9,21 +11,44 @@ require 'time'
|
|
9
11
|
require 'ua_parser'
|
10
12
|
require 'evaluation_details'
|
11
13
|
require 'user_agent_parser/operating_system'
|
14
|
+
require 'user_persistent_storage_utils'
|
12
15
|
|
13
16
|
$fetch_from_server = 'fetch_from_server'
|
14
17
|
$type_dynamic_config = 'dynamic_config'
|
15
18
|
|
16
19
|
module Statsig
|
17
20
|
class Evaluator
|
21
|
+
extend T::Sig
|
22
|
+
|
23
|
+
sig { returns(SpecStore) }
|
18
24
|
attr_accessor :spec_store
|
19
25
|
|
20
|
-
|
26
|
+
sig { returns(StatsigOptions) }
|
27
|
+
attr_accessor :options
|
28
|
+
|
29
|
+
sig { returns(UserPersistentStorageUtils) }
|
30
|
+
attr_accessor :persistent_storage_utils
|
31
|
+
|
32
|
+
sig do
|
33
|
+
params(
|
34
|
+
network: Network,
|
35
|
+
options: StatsigOptions,
|
36
|
+
error_callback: T.any(Method, Proc, NilClass),
|
37
|
+
diagnostics: Diagnostics,
|
38
|
+
error_boundary: ErrorBoundary,
|
39
|
+
logger: StatsigLogger,
|
40
|
+
persistent_storage_utils: UserPersistentStorageUtils,
|
41
|
+
).void
|
42
|
+
end
|
43
|
+
def initialize(network, options, error_callback, diagnostics, error_boundary, logger, persistent_storage_utils)
|
21
44
|
@spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics, error_boundary, logger)
|
22
45
|
UAParser.initialize_async
|
23
46
|
CountryLookup.initialize_async
|
24
47
|
|
25
48
|
@gate_overrides = {}
|
26
49
|
@config_overrides = {}
|
50
|
+
@options = options
|
51
|
+
@persistent_storage_utils = persistent_storage_utils
|
27
52
|
end
|
28
53
|
|
29
54
|
def maybe_restart_background_threads
|
@@ -52,7 +77,8 @@ module Statsig
|
|
52
77
|
eval_spec(user, @spec_store.get_gate(gate_name))
|
53
78
|
end
|
54
79
|
|
55
|
-
|
80
|
+
sig { params(user: StatsigUser, config_name: String, user_persisted_values: T.nilable(UserPersistedValues)).returns(ConfigResult) }
|
81
|
+
def get_config(user, config_name, user_persisted_values: nil)
|
56
82
|
if @config_overrides.key?(config_name)
|
57
83
|
id_type = @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name)['idType'] : ''
|
58
84
|
return Statsig::ConfigResult.new(
|
@@ -83,7 +109,26 @@ module Statsig
|
|
83
109
|
)
|
84
110
|
end
|
85
111
|
|
86
|
-
|
112
|
+
config = @spec_store.get_config(config_name)
|
113
|
+
|
114
|
+
# If persisted values is provided and the experiment is active, return sticky values if exists.
|
115
|
+
if !user_persisted_values.nil? && config['isActive'] == true
|
116
|
+
sticky_result = Statsig::ConfigResult.from_user_persisted_values(config_name, user_persisted_values)
|
117
|
+
return sticky_result unless sticky_result.nil?
|
118
|
+
|
119
|
+
# If it doesn't exist, then save to persisted storage if the user was assigned to an experiment group.
|
120
|
+
evaluation = eval_spec(user, config)
|
121
|
+
if evaluation.is_experiment_group
|
122
|
+
@persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name, evaluation)
|
123
|
+
@persistent_storage_utils.save_to_storage(user, config['idType'], user_persisted_values)
|
124
|
+
end
|
125
|
+
# Otherwise, remove from persisted storage
|
126
|
+
else
|
127
|
+
@persistent_storage_utils.remove_experiment_from_storage(user, config['idType'], config_name)
|
128
|
+
evaluation = eval_spec(user, config)
|
129
|
+
end
|
130
|
+
|
131
|
+
return evaluation
|
87
132
|
end
|
88
133
|
|
89
134
|
def get_layer(user, layer_name)
|
@@ -150,6 +195,7 @@ module Statsig
|
|
150
195
|
@config_overrides[config] = value
|
151
196
|
end
|
152
197
|
|
198
|
+
sig { params(user: StatsigUser, config: Hash).returns(ConfigResult) }
|
153
199
|
def eval_spec(user, config)
|
154
200
|
default_rule_id = 'default'
|
155
201
|
exposures = []
|
@@ -266,7 +312,7 @@ module Statsig
|
|
266
312
|
operator = condition['operator']
|
267
313
|
additional_values = condition['additionalValues']
|
268
314
|
additional_values = Hash.new unless additional_values.is_a? Hash
|
269
|
-
|
315
|
+
id_type = condition['idType']
|
270
316
|
|
271
317
|
return $fetch_from_server unless type.is_a? String
|
272
318
|
type = type.downcase
|
@@ -304,14 +350,14 @@ module Statsig
|
|
304
350
|
when 'user_bucket'
|
305
351
|
begin
|
306
352
|
salt = additional_values['salt']
|
307
|
-
unit_id = get_unit_id(
|
353
|
+
unit_id = user.get_unit_id(id_type) || ''
|
308
354
|
# there are only 1000 user buckets as opposed to 10k for gate pass %
|
309
355
|
value = compute_user_hash("#{salt}.#{unit_id}") % 1000
|
310
356
|
rescue
|
311
357
|
return false
|
312
358
|
end
|
313
359
|
when 'unit_id'
|
314
|
-
value = get_unit_id(
|
360
|
+
value = user.get_unit_id(id_type)
|
315
361
|
else
|
316
362
|
return $fetch_from_server
|
317
363
|
end
|
@@ -361,7 +407,7 @@ module Statsig
|
|
361
407
|
when 'none_case_sensitive'
|
362
408
|
return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
363
409
|
|
364
|
-
#string
|
410
|
+
# string
|
365
411
|
when 'str_starts_with_any'
|
366
412
|
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
|
367
413
|
when 'str_ends_with_any'
|
@@ -470,7 +516,7 @@ module Statsig
|
|
470
516
|
def eval_pass_percent(user, rule, config_salt)
|
471
517
|
return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
|
472
518
|
begin
|
473
|
-
unit_id = get_unit_id(
|
519
|
+
unit_id = user.get_unit_id(rule['idType']) || ''
|
474
520
|
rule_salt = rule['salt'] || rule['id'] || ''
|
475
521
|
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
476
522
|
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
@@ -479,14 +525,6 @@ module Statsig
|
|
479
525
|
end
|
480
526
|
end
|
481
527
|
|
482
|
-
def get_unit_id(user, id_type)
|
483
|
-
if id_type.is_a?(String) && id_type.downcase != 'userid'
|
484
|
-
return nil unless user&.custom_ids.is_a? Hash
|
485
|
-
return user.custom_ids[id_type] || user.custom_ids[id_type.downcase]
|
486
|
-
end
|
487
|
-
user.user_id
|
488
|
-
end
|
489
|
-
|
490
528
|
def compute_user_hash(user_hash)
|
491
529
|
Digest::SHA256.digest(user_hash).unpack('Q>')[0]
|
492
530
|
end
|
data/lib/id_list.rb
CHANGED
data/lib/layer.rb
CHANGED
@@ -17,11 +17,25 @@ class Layer
|
|
17
17
|
sig { returns(String) }
|
18
18
|
attr_accessor :rule_id
|
19
19
|
|
20
|
-
sig {
|
21
|
-
|
20
|
+
sig { returns(String) }
|
21
|
+
attr_accessor :group_name
|
22
|
+
|
23
|
+
sig do
|
24
|
+
params(
|
25
|
+
name: String,
|
26
|
+
value: T::Hash[String, T.untyped],
|
27
|
+
rule_id: String,
|
28
|
+
group_name: T.nilable(String),
|
29
|
+
allocated_experiment: T.nilable(String),
|
30
|
+
exposure_log_func: T.any(Method, Proc, NilClass)
|
31
|
+
).void
|
32
|
+
end
|
33
|
+
def initialize(name, value = {}, rule_id = '', group_name = nil, allocated_experiment = nil, exposure_log_func = nil)
|
22
34
|
@name = name
|
23
35
|
@value = value
|
24
36
|
@rule_id = rule_id
|
37
|
+
@group_name = group_name
|
38
|
+
@allocated_experiment = allocated_experiment
|
25
39
|
@exposure_log_func = exposure_log_func
|
26
40
|
end
|
27
41
|
|
@@ -58,4 +72,4 @@ class Layer
|
|
58
72
|
|
59
73
|
@value[index]
|
60
74
|
end
|
61
|
-
end
|
75
|
+
end
|
data/lib/network.rb
CHANGED
@@ -53,11 +53,42 @@ module Statsig
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
sig do
|
57
|
+
params(since_time: Integer)
|
58
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
59
|
+
end
|
60
|
+
def download_config_specs(since_time)
|
61
|
+
get("download_config_specs/#{@server_secret}.json?sinceTime=#{since_time}")
|
62
|
+
end
|
63
|
+
|
64
|
+
class HttpMethod < T::Enum
|
65
|
+
enums do
|
66
|
+
GET = new
|
67
|
+
POST = new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
sig do
|
72
|
+
params(endpoint: String, retries: Integer, backoff: Integer)
|
73
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
74
|
+
end
|
75
|
+
def get(endpoint, retries = 0, backoff = 1)
|
76
|
+
request(HttpMethod::GET, endpoint, nil, retries, backoff)
|
77
|
+
end
|
78
|
+
|
56
79
|
sig do
|
57
80
|
params(endpoint: String, body: String, retries: Integer, backoff: Integer)
|
58
81
|
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
59
82
|
end
|
60
|
-
def
|
83
|
+
def post(endpoint, body, retries = 0, backoff = 1)
|
84
|
+
request(HttpMethod::POST, endpoint, body, retries, backoff)
|
85
|
+
end
|
86
|
+
|
87
|
+
sig do
|
88
|
+
params(method: HttpMethod, endpoint: String, body: T.nilable(String), retries: Integer, backoff: Integer)
|
89
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
90
|
+
end
|
91
|
+
def request(method, endpoint, body, retries = 0, backoff = 1)
|
61
92
|
if @local_mode
|
62
93
|
return nil, nil
|
63
94
|
end
|
@@ -73,14 +104,20 @@ module Statsig
|
|
73
104
|
url = URIHelper.build_url(endpoint)
|
74
105
|
begin
|
75
106
|
res = @connection_pool.with do |conn|
|
76
|
-
conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
|
107
|
+
request = conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
|
108
|
+
case method
|
109
|
+
when HttpMethod::GET
|
110
|
+
request.get(url)
|
111
|
+
when HttpMethod::POST
|
112
|
+
request.post(url, body: body)
|
113
|
+
end
|
77
114
|
end
|
78
115
|
rescue StandardError => e
|
79
116
|
## network error retry
|
80
117
|
return nil, e unless retries.positive?
|
81
118
|
|
82
119
|
sleep backoff_adjusted
|
83
|
-
return
|
120
|
+
return request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
84
121
|
end
|
85
122
|
return res, nil if res.status.success?
|
86
123
|
|
@@ -91,12 +128,12 @@ module Statsig
|
|
91
128
|
|
92
129
|
## status code retry
|
93
130
|
sleep backoff_adjusted
|
94
|
-
|
131
|
+
request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
95
132
|
end
|
96
133
|
|
97
134
|
def check_gate(user, gate_name)
|
98
135
|
request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
|
99
|
-
response, =
|
136
|
+
response, = post('check_gate', request_body)
|
100
137
|
return JSON.parse(response.body) unless response.nil?
|
101
138
|
|
102
139
|
false
|
@@ -106,7 +143,7 @@ module Statsig
|
|
106
143
|
|
107
144
|
def get_config(user, dynamic_config_name)
|
108
145
|
request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
|
109
|
-
response, =
|
146
|
+
response, = post('get_config', request_body)
|
110
147
|
return JSON.parse(response.body) unless response.nil?
|
111
148
|
|
112
149
|
nil
|
@@ -116,7 +153,7 @@ module Statsig
|
|
116
153
|
|
117
154
|
def post_logs(events)
|
118
155
|
json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
|
119
|
-
|
156
|
+
post('log_event', json_body, @post_logs_retry_limit)
|
120
157
|
rescue StandardError
|
121
158
|
|
122
159
|
end
|
data/lib/spec_store.rb
CHANGED
@@ -4,6 +4,7 @@ require 'uri'
|
|
4
4
|
require 'evaluation_details'
|
5
5
|
require 'id_list'
|
6
6
|
require 'concurrent-ruby'
|
7
|
+
require 'hash_utils'
|
7
8
|
|
8
9
|
module Statsig
|
9
10
|
class SpecStore
|
@@ -28,7 +29,8 @@ module Statsig
|
|
28
29
|
:layers => {},
|
29
30
|
:id_lists => {},
|
30
31
|
:experiment_to_layer => {},
|
31
|
-
:sdk_keys_to_app_ids => {}
|
32
|
+
:sdk_keys_to_app_ids => {},
|
33
|
+
:hashed_sdk_keys_to_app_ids => {}
|
32
34
|
}
|
33
35
|
@diagnostics = diagnostics
|
34
36
|
@error_boundary = error_boundary
|
@@ -127,10 +129,18 @@ module Statsig
|
|
127
129
|
@specs[:sdk_keys_to_app_ids].key?(sdk_key)
|
128
130
|
end
|
129
131
|
|
132
|
+
def has_hashed_sdk_key?(hashed_sdk_key)
|
133
|
+
@specs[:hashed_sdk_keys_to_app_ids].key?(hashed_sdk_key)
|
134
|
+
end
|
135
|
+
|
130
136
|
def get_app_id_for_sdk_key(sdk_key)
|
131
137
|
if sdk_key.nil?
|
132
138
|
return nil
|
133
139
|
end
|
140
|
+
hashed_sdk_key = Statsig::HashUtils.djb2(sdk_key)
|
141
|
+
if has_hashed_sdk_key?(hashed_sdk_key)
|
142
|
+
return @specs[:hashed_sdk_keys_to_app_ids][hashed_sdk_key]
|
143
|
+
end
|
134
144
|
return nil unless has_sdk_key?(sdk_key)
|
135
145
|
@specs[:sdk_keys_to_app_ids][sdk_key]
|
136
146
|
end
|
@@ -228,7 +238,7 @@ module Statsig
|
|
228
238
|
|
229
239
|
error = nil
|
230
240
|
begin
|
231
|
-
response, e = @network.
|
241
|
+
response, e = @network.download_config_specs(@last_config_sync_time)
|
232
242
|
code = response&.status.to_i
|
233
243
|
if e.is_a? NetworkError
|
234
244
|
code = e.http_code
|
@@ -293,6 +303,7 @@ module Statsig
|
|
293
303
|
@specs[:layers] = new_layers
|
294
304
|
@specs[:experiment_to_layer] = new_exp_to_layer
|
295
305
|
@specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
|
306
|
+
@specs[:hashed_sdk_keys_to_app_ids] = specs_json['hashed_sdk_keys_to_app_ids'] || {}
|
296
307
|
|
297
308
|
unless from_adapter
|
298
309
|
save_config_specs_to_storage_adapter(specs_string)
|
@@ -323,7 +334,7 @@ module Statsig
|
|
323
334
|
|
324
335
|
def get_id_lists_from_network
|
325
336
|
tracker = @diagnostics.track('get_id_list_sources', 'network_request')
|
326
|
-
response, e = @network.
|
337
|
+
response, e = @network.post('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
|
327
338
|
code = response&.status.to_i
|
328
339
|
if e.is_a? NetworkError
|
329
340
|
code = e.http_code
|
data/lib/statsig.rb
CHANGED
@@ -26,15 +26,20 @@ module Statsig
|
|
26
26
|
@shared_instance = StatsigDriver.new(secret_key, options, error_callback)
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
class CheckGateOptions < T::Struct
|
30
|
+
prop :disable_log_exposure, T::Boolean, default: false
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { params(user: StatsigUser, gate_name: String, options: CheckGateOptions).returns(T::Boolean) }
|
30
34
|
##
|
31
35
|
# Gets the boolean result of a gate, evaluated against the given user. An exposure event will automatically be logged for the gate.
|
32
36
|
#
|
33
37
|
# @param user A StatsigUser object used for the evaluation
|
34
38
|
# @param gate_name The name of the gate being checked
|
35
|
-
|
39
|
+
# @param options Additional options for evaluating the gate
|
40
|
+
def self.check_gate(user, gate_name, options = CheckGateOptions.new)
|
36
41
|
ensure_initialized
|
37
|
-
@shared_instance&.check_gate(user, gate_name)
|
42
|
+
@shared_instance&.check_gate(user, gate_name, options)
|
38
43
|
end
|
39
44
|
|
40
45
|
sig { params(user: StatsigUser, gate_name: String).returns(T::Boolean) }
|
@@ -45,7 +50,7 @@ module Statsig
|
|
45
50
|
# @param gate_name The name of the gate being checked
|
46
51
|
def self.check_gate_with_exposure_logging_disabled(user, gate_name)
|
47
52
|
ensure_initialized
|
48
|
-
@shared_instance&.check_gate(user, gate_name,
|
53
|
+
@shared_instance&.check_gate(user, gate_name, CheckGateOptions.new(disable_log_exposure: true))
|
49
54
|
end
|
50
55
|
|
51
56
|
sig { params(user: StatsigUser, gate_name: String).void }
|
@@ -59,15 +64,20 @@ module Statsig
|
|
59
64
|
@shared_instance&.manually_log_gate_exposure(user, gate_name)
|
60
65
|
end
|
61
66
|
|
62
|
-
|
67
|
+
class GetConfigOptions < T::Struct
|
68
|
+
prop :disable_log_exposure, T::Boolean, default: false
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { params(user: StatsigUser, dynamic_config_name: String, options: GetConfigOptions).returns(DynamicConfig) }
|
63
72
|
##
|
64
73
|
# Get the values of a dynamic config, evaluated against the given user. An exposure event will automatically be logged for the dynamic config.
|
65
74
|
#
|
66
75
|
# @param user A StatsigUser object used for the evaluation
|
67
76
|
# @param dynamic_config_name The name of the dynamic config
|
68
|
-
|
77
|
+
# @param options Additional options for evaluating the config
|
78
|
+
def self.get_config(user, dynamic_config_name, options = GetConfigOptions.new)
|
69
79
|
ensure_initialized
|
70
|
-
@shared_instance&.get_config(user, dynamic_config_name)
|
80
|
+
@shared_instance&.get_config(user, dynamic_config_name, options)
|
71
81
|
end
|
72
82
|
|
73
83
|
sig { params(user: StatsigUser, dynamic_config_name: String).returns(DynamicConfig) }
|
@@ -78,7 +88,7 @@ module Statsig
|
|
78
88
|
# @param dynamic_config_name The name of the dynamic config
|
79
89
|
def self.get_config_with_exposure_logging_disabled(user, dynamic_config_name)
|
80
90
|
ensure_initialized
|
81
|
-
@shared_instance&.get_config(user, dynamic_config_name,
|
91
|
+
@shared_instance&.get_config(user, dynamic_config_name, GetConfigOptions.new(disable_log_exposure: true))
|
82
92
|
end
|
83
93
|
|
84
94
|
sig { params(user: StatsigUser, dynamic_config: String).void }
|
@@ -92,15 +102,21 @@ module Statsig
|
|
92
102
|
@shared_instance&.manually_log_config_exposure(user, dynamic_config)
|
93
103
|
end
|
94
104
|
|
95
|
-
|
105
|
+
class GetExperimentOptions < T::Struct
|
106
|
+
prop :disable_log_exposure, T::Boolean, default: false
|
107
|
+
prop :user_persisted_values, T.nilable(T::Hash[String, Hash]), default: nil
|
108
|
+
end
|
109
|
+
|
110
|
+
sig { params(user: StatsigUser, experiment_name: String, options: GetExperimentOptions).returns(DynamicConfig) }
|
96
111
|
##
|
97
112
|
# Get the values of an experiment, evaluated against the given user. An exposure event will automatically be logged for the experiment.
|
98
113
|
#
|
99
114
|
# @param user A StatsigUser object used for the evaluation
|
100
115
|
# @param experiment_name The name of the experiment
|
101
|
-
|
116
|
+
# @param options Additional options for evaluating the experiment
|
117
|
+
def self.get_experiment(user, experiment_name, options = GetExperimentOptions.new)
|
102
118
|
ensure_initialized
|
103
|
-
@shared_instance&.get_experiment(user, experiment_name)
|
119
|
+
@shared_instance&.get_experiment(user, experiment_name, options)
|
104
120
|
end
|
105
121
|
|
106
122
|
sig { params(user: StatsigUser, experiment_name: String).returns(DynamicConfig) }
|
@@ -111,7 +127,7 @@ module Statsig
|
|
111
127
|
# @param experiment_name The name of the experiment
|
112
128
|
def self.get_experiment_with_exposure_logging_disabled(user, experiment_name)
|
113
129
|
ensure_initialized
|
114
|
-
@shared_instance&.get_experiment(user, experiment_name,
|
130
|
+
@shared_instance&.get_experiment(user, experiment_name, GetExperimentOptions.new(disable_log_exposure: true))
|
115
131
|
end
|
116
132
|
|
117
133
|
sig { params(user: StatsigUser, experiment_name: String).void }
|
@@ -125,16 +141,26 @@ module Statsig
|
|
125
141
|
@shared_instance&.manually_log_config_exposure(user, experiment_name)
|
126
142
|
end
|
127
143
|
|
128
|
-
sig { params(user: StatsigUser,
|
144
|
+
sig { params(user: StatsigUser, id_type: String).returns(UserPersistedValues) }
|
145
|
+
def self.get_user_persisted_values(user, id_type)
|
146
|
+
ensure_initialized
|
147
|
+
@shared_instance&.get_user_persisted_values(user, id_type)
|
148
|
+
end
|
149
|
+
|
150
|
+
class GetLayerOptions < T::Struct
|
151
|
+
prop :disable_log_exposure, T::Boolean, default: false
|
152
|
+
end
|
153
|
+
|
154
|
+
sig { params(user: StatsigUser, layer_name: String, options: GetLayerOptions).returns(Layer) }
|
129
155
|
##
|
130
156
|
# Get the values of a layer, evaluated against the given user.
|
131
157
|
# Exposure events will be fired when get or get_typed is called on the resulting Layer class.
|
132
158
|
#
|
133
159
|
# @param user A StatsigUser object used for the evaluation
|
134
160
|
# @param layer_name The name of the layer
|
135
|
-
def self.get_layer(user, layer_name)
|
161
|
+
def self.get_layer(user, layer_name, options = GetLayerOptions.new)
|
136
162
|
ensure_initialized
|
137
|
-
@shared_instance&.get_layer(user, layer_name)
|
163
|
+
@shared_instance&.get_layer(user, layer_name, options)
|
138
164
|
end
|
139
165
|
|
140
166
|
sig { params(user: StatsigUser, layer_name: String).returns(Layer) }
|
@@ -145,7 +171,7 @@ module Statsig
|
|
145
171
|
# @param layer_name The name of the layer
|
146
172
|
def self.get_layer_with_exposure_logging_disabled(user, layer_name)
|
147
173
|
ensure_initialized
|
148
|
-
@shared_instance&.get_layer(user, layer_name,
|
174
|
+
@shared_instance&.get_layer(user, layer_name, GetLayerOptions.new(disable_log_exposure: true))
|
149
175
|
end
|
150
176
|
|
151
177
|
sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
|
@@ -239,7 +265,7 @@ module Statsig
|
|
239
265
|
def self.get_statsig_metadata
|
240
266
|
{
|
241
267
|
'sdkType' => 'ruby-server',
|
242
|
-
'sdkVersion' => '1.
|
268
|
+
'sdkVersion' => '1.29.0',
|
243
269
|
}
|
244
270
|
end
|
245
271
|
|
@@ -252,7 +278,6 @@ module Statsig
|
|
252
278
|
end
|
253
279
|
|
254
280
|
sig { params(options: T.any(StatsigOptions, NilClass)).void }
|
255
|
-
|
256
281
|
def self.bind_sorbet_loggers(options)
|
257
282
|
if options&.disable_sorbet_logging_handlers == true
|
258
283
|
return
|
data/lib/statsig_driver.rb
CHANGED
@@ -19,7 +19,6 @@ class StatsigDriver
|
|
19
19
|
extend T::Sig
|
20
20
|
|
21
21
|
sig { params(secret_key: String, options: T.any(StatsigOptions, NilClass), error_callback: T.any(Method, Proc, NilClass)).void }
|
22
|
-
|
23
22
|
def initialize(secret_key, options = nil, error_callback = nil)
|
24
23
|
unless secret_key.start_with?('secret-')
|
25
24
|
raise Statsig::ValueError.new('Invalid secret key provided. Provide your project secret key from the Statsig console')
|
@@ -38,20 +37,16 @@ class StatsigDriver
|
|
38
37
|
@secret_key = secret_key
|
39
38
|
@net = Statsig::Network.new(secret_key, @options)
|
40
39
|
@logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
|
41
|
-
@
|
40
|
+
@persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
|
41
|
+
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, @persistent_storage_utils)
|
42
42
|
tracker.end(success: true)
|
43
43
|
|
44
44
|
@logger.log_diagnostics_event(@diagnostics)
|
45
45
|
}, caller: __method__.to_s)
|
46
46
|
end
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
|
52
|
-
sig { params(user: StatsigUser, gate_name: String, options: CheckGateOptions).returns(T::Boolean) }
|
53
|
-
|
54
|
-
def check_gate(user, gate_name, options = CheckGateOptions.new)
|
48
|
+
sig { params(user: StatsigUser, gate_name: String, options: Statsig::CheckGateOptions).returns(T::Boolean) }
|
49
|
+
def check_gate(user, gate_name, options = Statsig::CheckGateOptions.new)
|
55
50
|
@err_boundary.capture(task: lambda {
|
56
51
|
run_with_diagnostics(task: lambda {
|
57
52
|
user = verify_inputs(user, gate_name, "gate_name")
|
@@ -65,7 +60,7 @@ class StatsigDriver
|
|
65
60
|
res = check_gate_fallback(user, gate_name)
|
66
61
|
# exposure logged by the server
|
67
62
|
else
|
68
|
-
if options.
|
63
|
+
if !options.disable_log_exposure
|
69
64
|
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
70
65
|
end
|
71
66
|
end
|
@@ -76,7 +71,6 @@ class StatsigDriver
|
|
76
71
|
end
|
77
72
|
|
78
73
|
sig { params(user: StatsigUser, gate_name: String).void }
|
79
|
-
|
80
74
|
def manually_log_gate_exposure(user, gate_name)
|
81
75
|
@err_boundary.capture(task: lambda {
|
82
76
|
res = @evaluator.check_gate(user, gate_name)
|
@@ -85,38 +79,27 @@ class StatsigDriver
|
|
85
79
|
})
|
86
80
|
end
|
87
81
|
|
88
|
-
|
89
|
-
|
90
|
-
end
|
91
|
-
|
92
|
-
sig { params(user: StatsigUser, dynamic_config_name: String, options: GetConfigOptions).returns(DynamicConfig) }
|
93
|
-
|
94
|
-
def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
|
82
|
+
sig { params(user: StatsigUser, dynamic_config_name: String, options: Statsig::GetConfigOptions).returns(DynamicConfig) }
|
83
|
+
def get_config(user, dynamic_config_name, options = Statsig::GetConfigOptions.new)
|
95
84
|
@err_boundary.capture(task: lambda {
|
96
85
|
run_with_diagnostics(task: lambda {
|
97
86
|
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
98
|
-
get_config_impl(user, dynamic_config_name, options)
|
87
|
+
get_config_impl(user, dynamic_config_name, options.disable_log_exposure)
|
99
88
|
}, caller: __method__.to_s)
|
100
89
|
}, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
|
101
90
|
end
|
102
91
|
|
103
|
-
|
104
|
-
|
105
|
-
end
|
106
|
-
|
107
|
-
sig { params(user: StatsigUser, experiment_name: String, options: GetExperimentOptions).returns(DynamicConfig) }
|
108
|
-
|
109
|
-
def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
|
92
|
+
sig { params(user: StatsigUser, experiment_name: String, options: Statsig::GetExperimentOptions).returns(DynamicConfig) }
|
93
|
+
def get_experiment(user, experiment_name, options = Statsig::GetExperimentOptions.new)
|
110
94
|
@err_boundary.capture(task: lambda {
|
111
95
|
run_with_diagnostics(task: lambda {
|
112
96
|
user = verify_inputs(user, experiment_name, "experiment_name")
|
113
|
-
get_config_impl(user, experiment_name, options)
|
97
|
+
get_config_impl(user, experiment_name, options.disable_log_exposure, user_persisted_values: options.user_persisted_values)
|
114
98
|
}, caller: __method__.to_s)
|
115
99
|
}, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
|
116
100
|
end
|
117
101
|
|
118
102
|
sig { params(user: StatsigUser, config_name: String).void }
|
119
|
-
|
120
103
|
def manually_log_config_exposure(user, config_name)
|
121
104
|
@err_boundary.capture(task: lambda {
|
122
105
|
res = @evaluator.get_config(user, config_name)
|
@@ -125,13 +108,18 @@ class StatsigDriver
|
|
125
108
|
}, caller: __method__.to_s)
|
126
109
|
end
|
127
110
|
|
128
|
-
|
129
|
-
|
130
|
-
|
111
|
+
sig { params(user: StatsigUser, id_type: String).returns(Statsig::UserPersistedValues) }
|
112
|
+
def get_user_persisted_values(user, id_type)
|
113
|
+
@err_boundary.capture(task: lambda {
|
114
|
+
persisted_values = @persistent_storage_utils.get_user_persisted_values(user, id_type)
|
115
|
+
return {} if persisted_values.nil?
|
131
116
|
|
132
|
-
|
117
|
+
persisted_values
|
118
|
+
}, caller: __method__.to_s)
|
119
|
+
end
|
133
120
|
|
134
|
-
|
121
|
+
sig { params(user: StatsigUser, layer_name: String, options: Statsig::GetLayerOptions).returns(Layer) }
|
122
|
+
def get_layer(user, layer_name, options = Statsig::GetLayerOptions.new)
|
135
123
|
@err_boundary.capture(task: lambda {
|
136
124
|
run_with_diagnostics(task: lambda {
|
137
125
|
user = verify_inputs(user, layer_name, "layer_name")
|
@@ -142,27 +130,26 @@ class StatsigDriver
|
|
142
130
|
end
|
143
131
|
|
144
132
|
if res == $fetch_from_server
|
145
|
-
if res.config_delegate.
|
133
|
+
if res.config_delegate.nil?
|
146
134
|
return Layer.new(layer_name)
|
147
135
|
end
|
148
136
|
res = get_config_fallback(user, res.config_delegate)
|
149
137
|
# exposure logged by the server
|
150
138
|
end
|
151
139
|
|
152
|
-
exposure_log_func = options.
|
140
|
+
exposure_log_func = !options.disable_log_exposure ? lambda { |layer, parameter_name|
|
153
141
|
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
154
142
|
} : nil
|
155
|
-
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
143
|
+
Layer.new(res.name, res.json_value, res.rule_id, res.group_name, res.config_delegate, exposure_log_func)
|
156
144
|
}, caller: __method__.to_s)
|
157
145
|
}, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
|
158
146
|
end
|
159
147
|
|
160
148
|
sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
|
161
|
-
|
162
149
|
def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
163
150
|
@err_boundary.capture(task: lambda {
|
164
151
|
res = @evaluator.get_layer(user, layer_name)
|
165
|
-
layer = Layer.new(layer_name, res.json_value, res.rule_id)
|
152
|
+
layer = Layer.new(layer_name, res.json_value, res.rule_id, res.group_name, res.config_delegate)
|
166
153
|
context = { 'is_manual_exposure' => true }
|
167
154
|
@logger.log_layer_exposure(user, layer, parameter_name, res, context)
|
168
155
|
}, caller: __method__.to_s)
|
@@ -260,7 +247,6 @@ class StatsigDriver
|
|
260
247
|
end
|
261
248
|
|
262
249
|
sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
|
263
|
-
|
264
250
|
def verify_inputs(user, config_name, variable_name)
|
265
251
|
validate_user(user)
|
266
252
|
if !config_name.is_a?(String) || config_name.empty?
|
@@ -272,8 +258,16 @@ class StatsigDriver
|
|
272
258
|
normalize_user(user)
|
273
259
|
end
|
274
260
|
|
275
|
-
|
276
|
-
|
261
|
+
sig do
|
262
|
+
params(
|
263
|
+
user: StatsigUser,
|
264
|
+
config_name: String,
|
265
|
+
disable_log_exposure: T::Boolean,
|
266
|
+
user_persisted_values: T.nilable(Statsig::UserPersistedValues)
|
267
|
+
).returns(DynamicConfig)
|
268
|
+
end
|
269
|
+
def get_config_impl(user, config_name, disable_log_exposure, user_persisted_values: nil)
|
270
|
+
res = @evaluator.get_config(user, config_name, user_persisted_values: user_persisted_values)
|
277
271
|
if res.nil?
|
278
272
|
res = Statsig::ConfigResult.new(config_name)
|
279
273
|
end
|
@@ -282,12 +276,12 @@ class StatsigDriver
|
|
282
276
|
res = get_config_fallback(user, config_name)
|
283
277
|
# exposure logged by the server
|
284
278
|
else
|
285
|
-
if
|
279
|
+
if !disable_log_exposure
|
286
280
|
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
287
281
|
end
|
288
282
|
end
|
289
283
|
|
290
|
-
DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type)
|
284
|
+
DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type, res.evaluation_details)
|
291
285
|
end
|
292
286
|
|
293
287
|
def validate_user(user)
|
data/lib/statsig_options.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'sorbet-runtime'
|
4
4
|
require_relative 'interfaces/data_store'
|
5
|
+
require_relative 'interfaces/user_persistent_storage'
|
5
6
|
|
6
7
|
##
|
7
8
|
# Configuration options for the Statsig SDK.
|
@@ -21,7 +22,7 @@ class StatsigOptions
|
|
21
22
|
|
22
23
|
# The base url used specifically to call download_config_specs.
|
23
24
|
# Takes precedence over api_url_base
|
24
|
-
sig { returns(
|
25
|
+
sig { returns(String) }
|
25
26
|
attr_accessor :api_url_download_config_specs
|
26
27
|
|
27
28
|
sig { returns(T.any(Float, Integer)) }
|
@@ -106,10 +107,15 @@ class StatsigOptions
|
|
106
107
|
# which overrides the default backoff time between retries
|
107
108
|
attr_accessor :post_logs_retry_backoff
|
108
109
|
|
110
|
+
sig { returns(T.any(Statsig::Interfaces::IUserPersistentStorage, NilClass)) }
|
111
|
+
# A storage adapter for persisted values. Can be used for sticky bucketing users in experiments.
|
112
|
+
# Implements Statsig::Interfaces::IUserPersistentStorage.
|
113
|
+
attr_accessor :user_persistent_storage
|
114
|
+
|
109
115
|
sig do
|
110
116
|
params(
|
111
117
|
environment: T.any(T::Hash[String, String], NilClass),
|
112
|
-
api_url_base: String,
|
118
|
+
api_url_base: T.nilable(String),
|
113
119
|
api_url_download_config_specs: T.any(String, NilClass),
|
114
120
|
rulesets_sync_interval: T.any(Float, Integer),
|
115
121
|
idlists_sync_interval: T.any(Float, Integer),
|
@@ -127,13 +133,13 @@ class StatsigOptions
|
|
127
133
|
disable_sorbet_logging_handlers: T::Boolean,
|
128
134
|
network_timeout: T.any(Integer, NilClass),
|
129
135
|
post_logs_retry_limit: Integer,
|
130
|
-
post_logs_retry_backoff: T.any(Method, Proc, Integer, NilClass)
|
136
|
+
post_logs_retry_backoff: T.any(Method, Proc, Integer, NilClass),
|
137
|
+
user_persistent_storage: T.any(Statsig::Interfaces::IUserPersistentStorage, NilClass)
|
131
138
|
).void
|
132
139
|
end
|
133
|
-
|
134
140
|
def initialize(
|
135
141
|
environment = nil,
|
136
|
-
api_url_base =
|
142
|
+
api_url_base = nil,
|
137
143
|
api_url_download_config_specs: nil,
|
138
144
|
rulesets_sync_interval: 10,
|
139
145
|
idlists_sync_interval: 60,
|
@@ -151,10 +157,12 @@ class StatsigOptions
|
|
151
157
|
disable_sorbet_logging_handlers: false,
|
152
158
|
network_timeout: nil,
|
153
159
|
post_logs_retry_limit: 3,
|
154
|
-
post_logs_retry_backoff: nil
|
160
|
+
post_logs_retry_backoff: nil,
|
161
|
+
user_persistent_storage: nil
|
162
|
+
)
|
155
163
|
@environment = environment.is_a?(Hash) ? environment : nil
|
156
|
-
@api_url_base = api_url_base
|
157
|
-
@api_url_download_config_specs = api_url_download_config_specs
|
164
|
+
@api_url_base = api_url_base || 'https://statsigapi.net/v1'
|
165
|
+
@api_url_download_config_specs = api_url_download_config_specs || api_url_base || 'https://api.statsigcdn.com/v1'
|
158
166
|
@rulesets_sync_interval = rulesets_sync_interval
|
159
167
|
@idlists_sync_interval = idlists_sync_interval
|
160
168
|
@disable_rulesets_sync = disable_rulesets_sync
|
@@ -172,5 +180,7 @@ class StatsigOptions
|
|
172
180
|
@network_timeout = network_timeout
|
173
181
|
@post_logs_retry_limit = post_logs_retry_limit
|
174
182
|
@post_logs_retry_backoff = post_logs_retry_backoff
|
183
|
+
@user_persistent_storage = user_persistent_storage
|
184
|
+
|
175
185
|
end
|
176
|
-
end
|
186
|
+
end
|
data/lib/statsig_user.rb
CHANGED
@@ -49,7 +49,6 @@ class StatsigUser
|
|
49
49
|
attr_accessor :private_attributes
|
50
50
|
|
51
51
|
sig { returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
52
|
-
|
53
52
|
def custom
|
54
53
|
@custom
|
55
54
|
end
|
@@ -61,7 +60,6 @@ class StatsigUser
|
|
61
60
|
end
|
62
61
|
|
63
62
|
sig { params(user_hash: T.any(T::Hash[T.any(String, Symbol), T.untyped], NilClass)).void }
|
64
|
-
|
65
63
|
def initialize(user_hash)
|
66
64
|
the_hash = user_hash
|
67
65
|
begin
|
@@ -166,6 +164,15 @@ class StatsigUser
|
|
166
164
|
}
|
167
165
|
end
|
168
166
|
|
167
|
+
def get_unit_id(id_type)
|
168
|
+
if id_type.is_a?(String) && id_type.downcase != 'userid'
|
169
|
+
return nil unless @custom_ids.is_a? Hash
|
170
|
+
|
171
|
+
return @custom_ids[id_type] || @custom_ids[id_type.downcase]
|
172
|
+
end
|
173
|
+
@user_id
|
174
|
+
end
|
175
|
+
|
169
176
|
private
|
170
177
|
|
171
178
|
sig {
|
@@ -189,5 +196,4 @@ class StatsigUser
|
|
189
196
|
|
190
197
|
nil
|
191
198
|
end
|
192
|
-
|
193
|
-
end
|
199
|
+
end
|
data/lib/uri_helper.rb
CHANGED
@@ -17,7 +17,7 @@ class URIHelper
|
|
17
17
|
sig { params(endpoint: String).returns(String) }
|
18
18
|
def build_url(endpoint)
|
19
19
|
api = @options.api_url_base
|
20
|
-
if endpoint
|
20
|
+
if endpoint.include?('download_config_specs')
|
21
21
|
api = T.must(@options.api_url_download_config_specs)
|
22
22
|
end
|
23
23
|
unless api.end_with?('/')
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require 'statsig_options'
|
5
|
+
|
6
|
+
module Statsig
|
7
|
+
UserPersistedValues = T.type_alias { T::Hash[String, Hash] }
|
8
|
+
|
9
|
+
class UserPersistentStorageUtils
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(T::Hash[String, UserPersistedValues]) }
|
13
|
+
attr_accessor :cache
|
14
|
+
|
15
|
+
sig { returns(T.nilable(Interfaces::IUserPersistentStorage)) }
|
16
|
+
attr_accessor :storage
|
17
|
+
|
18
|
+
sig { params(options: StatsigOptions).void }
|
19
|
+
def initialize(options)
|
20
|
+
@storage = options.user_persistent_storage
|
21
|
+
@cache = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
sig { params(user: StatsigUser, id_type: String).returns(T.nilable(UserPersistedValues)) }
|
25
|
+
def get_user_persisted_values(user, id_type)
|
26
|
+
key = self.class.get_storage_key(user, id_type)
|
27
|
+
return @cache[key] unless @cache[key].nil?
|
28
|
+
|
29
|
+
return load_from_storage(key)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { params(key: String).returns(T.nilable(UserPersistedValues)) }
|
33
|
+
def load_from_storage(key)
|
34
|
+
return if @storage.nil?
|
35
|
+
|
36
|
+
begin
|
37
|
+
storage_values = @storage.load(key)
|
38
|
+
rescue StandardError => e
|
39
|
+
puts "Failed to load key (#{key}) from user_persisted_storage (#{e.message})"
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
|
43
|
+
unless storage_values.nil?
|
44
|
+
parsed_values = self.class.parse(storage_values)
|
45
|
+
unless parsed_values.nil?
|
46
|
+
@cache[key] = parsed_values
|
47
|
+
return @cache[key]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { params(user: StatsigUser, id_type: String, user_persisted_values: UserPersistedValues).void }
|
54
|
+
def save_to_storage(user, id_type, user_persisted_values)
|
55
|
+
return if @storage.nil?
|
56
|
+
|
57
|
+
key = self.class.get_storage_key(user, id_type)
|
58
|
+
stringified = self.class.stringify(user_persisted_values)
|
59
|
+
return if stringified.nil?
|
60
|
+
|
61
|
+
begin
|
62
|
+
@storage.save(key, stringified)
|
63
|
+
rescue StandardError => e
|
64
|
+
puts "Failed to save key (#{key}) to user_persisted_storage (#{e.message})"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(user: StatsigUser, id_type: String, config_name: String).void }
|
69
|
+
def remove_experiment_from_storage(user, id_type, config_name)
|
70
|
+
persisted_values = get_user_persisted_values(user, id_type)
|
71
|
+
unless persisted_values.nil?
|
72
|
+
persisted_values.delete(config_name)
|
73
|
+
save_to_storage(user, id_type, persisted_values)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { params(user_persisted_values: T.nilable(UserPersistedValues), config_name: String, evaluation: ConfigResult).void }
|
78
|
+
def add_evaluation_to_user_persisted_values(user_persisted_values, config_name, evaluation)
|
79
|
+
if user_persisted_values.nil?
|
80
|
+
user_persisted_values = {}
|
81
|
+
end
|
82
|
+
user_persisted_values[config_name] = evaluation.to_hash
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
sig { params(values_string: String).returns(T.nilable(UserPersistedValues)) }
|
88
|
+
def self.parse(values_string)
|
89
|
+
return JSON.parse(values_string)
|
90
|
+
rescue JSON::ParserError
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
sig { params(values_object: UserPersistedValues).returns(T.nilable(String)) }
|
95
|
+
def self.stringify(values_object)
|
96
|
+
return JSON.generate(values_object)
|
97
|
+
rescue StandardError
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
|
101
|
+
sig { params(user: StatsigUser, id_type: String).returns(String) }
|
102
|
+
def self.get_storage_key(user, id_type)
|
103
|
+
"#{user.get_unit_id(id_type)}:#{id_type}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
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.
|
4
|
+
version: 1.29.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: 2023-
|
11
|
+
date: 2023-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -333,6 +333,7 @@ files:
|
|
333
333
|
- lib/hash_utils.rb
|
334
334
|
- lib/id_list.rb
|
335
335
|
- lib/interfaces/data_store.rb
|
336
|
+
- lib/interfaces/user_persistent_storage.rb
|
336
337
|
- lib/layer.rb
|
337
338
|
- lib/network.rb
|
338
339
|
- lib/spec_store.rb
|
@@ -345,6 +346,7 @@ files:
|
|
345
346
|
- lib/statsig_user.rb
|
346
347
|
- lib/ua_parser.rb
|
347
348
|
- lib/uri_helper.rb
|
349
|
+
- lib/user_persistent_storage_utils.rb
|
348
350
|
homepage: https://rubygems.org/gems/statsig
|
349
351
|
licenses:
|
350
352
|
- ISC
|