statsig 1.27.0 → 1.29.0.pre.beta.1
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/client_initialize_helpers.rb +12 -2
- data/lib/config_result.rb +40 -1
- data/lib/dynamic_config.rb +15 -2
- data/lib/evaluation_details.rb +12 -7
- data/lib/evaluator.rb +57 -18
- data/lib/hash_utils.rb +15 -0
- data/lib/id_list.rb +1 -1
- data/lib/interfaces/user_persistent_storage.rb +12 -0
- data/lib/spec_store.rb +12 -1
- data/lib/statsig.rb +43 -18
- data/lib/statsig_driver.rb +34 -40
- data/lib/statsig_options.rb +14 -3
- data/lib/statsig_user.rb +53 -4
- data/lib/user_persistent_storage_utils.rb +106 -0
- metadata +10 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e279228e9eb701b7bcf06ac68ada05a4403f1d046c4ddc3343c87cf86d6be049
|
4
|
+
data.tar.gz: 3b110f44f460c9924aa909c1e9723094189a0fe4a20c6f7572c997cf40968e29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4871a883213685bc1d6c537367f4de78cacc81b0cec6929853b57c80439895e0ed49d797ef5bb7733c5dce73df9c0bb1aa5ef4b8c3563351468b557f3e473b4
|
7
|
+
data.tar.gz: fbdc2952ed68a92640268c6931b59521e641d22448c2c8a897521637951d7c20ea6f0f6d4354252c184a8a1c0b9e03861f9b047c9634165998f6a9218660e7e6
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# typed: true
|
2
2
|
|
3
3
|
require_relative 'hash_utils'
|
4
|
+
require 'sorbet-runtime'
|
4
5
|
|
5
6
|
$empty_eval_result = {
|
6
7
|
:gate_value => false,
|
@@ -12,6 +13,8 @@ $empty_eval_result = {
|
|
12
13
|
|
13
14
|
module ClientInitializeHelpers
|
14
15
|
class ResponseFormatter
|
16
|
+
extend T::Sig
|
17
|
+
|
15
18
|
def initialize(evaluator, user, hash, client_sdk_key)
|
16
19
|
@evaluator = evaluator
|
17
20
|
@user = user
|
@@ -28,6 +31,13 @@ module ClientInitializeHelpers
|
|
28
31
|
|
29
32
|
private
|
30
33
|
|
34
|
+
sig { params(secondary_exposures: T::Array[T::Hash[String, String]]).returns(T::Array[T::Hash[String, String]]) }
|
35
|
+
def filter_segments_from_secondary_exposures(secondary_exposures)
|
36
|
+
secondary_exposures.reject do |exposure|
|
37
|
+
exposure['gate'].to_s.start_with?('segment:')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
31
41
|
def to_response(config_name, config_spec)
|
32
42
|
target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
|
33
43
|
config_target_apps = config_spec['targetAppIDs']
|
@@ -49,7 +59,7 @@ module ClientInitializeHelpers
|
|
49
59
|
:id_type => eval_result.id_type,
|
50
60
|
:config_delegate => eval_result.config_delegate,
|
51
61
|
:is_experiment_group => eval_result.is_experiment_group,
|
52
|
-
:secondary_exposures => eval_result.secondary_exposures,
|
62
|
+
:secondary_exposures => filter_segments_from_secondary_exposures(eval_result.secondary_exposures),
|
53
63
|
:undelegated_sec_exps => eval_result.undelegated_sec_exps
|
54
64
|
}
|
55
65
|
|
@@ -94,7 +104,7 @@ module ClientInitializeHelpers
|
|
94
104
|
"name" => hashed_name,
|
95
105
|
"rule_id" => safe_eval_result[:rule_id],
|
96
106
|
"secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
|
97
|
-
})]
|
107
|
+
}).compact]
|
98
108
|
end
|
99
109
|
|
100
110
|
def clean_exposures(exposures)
|
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
|
@@ -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)
|
@@ -123,7 +168,8 @@ module Statsig
|
|
123
168
|
"generator" => "statsig-ruby-sdk",
|
124
169
|
"evaluated_keys" => evaluated_keys,
|
125
170
|
"time" => 0,
|
126
|
-
"hash_used" => hash
|
171
|
+
"hash_used" => hash,
|
172
|
+
"user_hash" => user.to_hash_without_stable_id()
|
127
173
|
}
|
128
174
|
end
|
129
175
|
|
@@ -149,6 +195,7 @@ module Statsig
|
|
149
195
|
@config_overrides[config] = value
|
150
196
|
end
|
151
197
|
|
198
|
+
sig { params(user: StatsigUser, config: Hash).returns(ConfigResult) }
|
152
199
|
def eval_spec(user, config)
|
153
200
|
default_rule_id = 'default'
|
154
201
|
exposures = []
|
@@ -200,7 +247,7 @@ module Statsig
|
|
200
247
|
@spec_store.initial_config_sync_time,
|
201
248
|
@spec_store.init_reason
|
202
249
|
),
|
203
|
-
group_name:
|
250
|
+
group_name: nil,
|
204
251
|
id_type: config['idType']
|
205
252
|
)
|
206
253
|
end
|
@@ -265,7 +312,7 @@ module Statsig
|
|
265
312
|
operator = condition['operator']
|
266
313
|
additional_values = condition['additionalValues']
|
267
314
|
additional_values = Hash.new unless additional_values.is_a? Hash
|
268
|
-
|
315
|
+
id_type = condition['idType']
|
269
316
|
|
270
317
|
return $fetch_from_server unless type.is_a? String
|
271
318
|
type = type.downcase
|
@@ -303,14 +350,14 @@ module Statsig
|
|
303
350
|
when 'user_bucket'
|
304
351
|
begin
|
305
352
|
salt = additional_values['salt']
|
306
|
-
unit_id = get_unit_id(
|
353
|
+
unit_id = user.get_unit_id(id_type) || ''
|
307
354
|
# there are only 1000 user buckets as opposed to 10k for gate pass %
|
308
355
|
value = compute_user_hash("#{salt}.#{unit_id}") % 1000
|
309
356
|
rescue
|
310
357
|
return false
|
311
358
|
end
|
312
359
|
when 'unit_id'
|
313
|
-
value = get_unit_id(
|
360
|
+
value = user.get_unit_id(id_type)
|
314
361
|
else
|
315
362
|
return $fetch_from_server
|
316
363
|
end
|
@@ -360,7 +407,7 @@ module Statsig
|
|
360
407
|
when 'none_case_sensitive'
|
361
408
|
return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
362
409
|
|
363
|
-
#string
|
410
|
+
# string
|
364
411
|
when 'str_starts_with_any'
|
365
412
|
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
|
366
413
|
when 'str_ends_with_any'
|
@@ -469,7 +516,7 @@ module Statsig
|
|
469
516
|
def eval_pass_percent(user, rule, config_salt)
|
470
517
|
return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
|
471
518
|
begin
|
472
|
-
unit_id = get_unit_id(
|
519
|
+
unit_id = user.get_unit_id(rule['idType']) || ''
|
473
520
|
rule_salt = rule['salt'] || rule['id'] || ''
|
474
521
|
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
475
522
|
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
@@ -478,14 +525,6 @@ module Statsig
|
|
478
525
|
end
|
479
526
|
end
|
480
527
|
|
481
|
-
def get_unit_id(user, id_type)
|
482
|
-
if id_type.is_a?(String) && id_type.downcase != 'userid'
|
483
|
-
return nil unless user&.custom_ids.is_a? Hash
|
484
|
-
return user.custom_ids[id_type] || user.custom_ids[id_type.downcase]
|
485
|
-
end
|
486
|
-
user.user_id
|
487
|
-
end
|
488
|
-
|
489
528
|
def compute_user_hash(user_hash)
|
490
529
|
Digest::SHA256.digest(user_hash).unpack('Q>')[0]
|
491
530
|
end
|
data/lib/hash_utils.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'json'
|
1
2
|
module Statsig
|
2
3
|
class HashUtils
|
3
4
|
def self.djb2(input_str)
|
@@ -10,8 +11,22 @@ module Statsig
|
|
10
11
|
return hash.to_s
|
11
12
|
end
|
12
13
|
|
14
|
+
def self.djb2ForHash(input_hash)
|
15
|
+
return djb2(input_hash.to_json)
|
16
|
+
end
|
17
|
+
|
13
18
|
def self.sha256(input_str)
|
14
19
|
return Digest::SHA256.base64digest(input_str)
|
15
20
|
end
|
21
|
+
|
22
|
+
def self.sortHash(input_hash)
|
23
|
+
dictionary = input_hash.clone.sort_by { |key| key }.to_h;
|
24
|
+
input_hash.each do |key, value|
|
25
|
+
if value.is_a?(Hash)
|
26
|
+
dictionary[key] = self.sortHash(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
return dictionary
|
30
|
+
end
|
16
31
|
end
|
17
32
|
end
|
data/lib/id_list.rb
CHANGED
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
|
@@ -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)
|
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-beta.1',
|
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")
|
@@ -149,7 +137,7 @@ class StatsigDriver
|
|
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
143
|
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
@@ -158,7 +146,6 @@ class StatsigDriver
|
|
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)
|
@@ -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.
|
@@ -106,6 +107,11 @@ 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),
|
@@ -127,7 +133,8 @@ 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
140
|
|
@@ -151,7 +158,9 @@ class StatsigOptions
|
|
151
158
|
disable_sorbet_logging_handlers: false,
|
152
159
|
network_timeout: nil,
|
153
160
|
post_logs_retry_limit: 3,
|
154
|
-
post_logs_retry_backoff: nil
|
161
|
+
post_logs_retry_backoff: nil,
|
162
|
+
user_persistent_storage: nil
|
163
|
+
)
|
155
164
|
@environment = environment.is_a?(Hash) ? environment : nil
|
156
165
|
@api_url_base = api_url_base
|
157
166
|
@api_url_download_config_specs = api_url_download_config_specs
|
@@ -172,5 +181,7 @@ class StatsigOptions
|
|
172
181
|
@network_timeout = network_timeout
|
173
182
|
@post_logs_retry_limit = post_logs_retry_limit
|
174
183
|
@post_logs_retry_backoff = post_logs_retry_backoff
|
184
|
+
@user_persistent_storage = user_persistent_storage
|
185
|
+
|
175
186
|
end
|
176
|
-
end
|
187
|
+
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
|
@@ -103,6 +101,49 @@ class StatsigUser
|
|
103
101
|
hash.compact
|
104
102
|
end
|
105
103
|
|
104
|
+
def to_hash_without_stable_id()
|
105
|
+
hash = {}
|
106
|
+
if @user_id != nil
|
107
|
+
hash['userID'] = @user_id
|
108
|
+
end
|
109
|
+
if @email != nil
|
110
|
+
hash['email'] = @email
|
111
|
+
end
|
112
|
+
if @ip != nil
|
113
|
+
hash['ip'] = @ip
|
114
|
+
end
|
115
|
+
if @user_agent != nil
|
116
|
+
hash['userAgent'] = @user_agent
|
117
|
+
end
|
118
|
+
if @country != nil
|
119
|
+
hash['country'] = @country
|
120
|
+
end
|
121
|
+
if @locale != nil
|
122
|
+
hash['locale'] = @locale
|
123
|
+
end
|
124
|
+
if @app_version != nil
|
125
|
+
hash['appVersion'] = @app_version
|
126
|
+
end
|
127
|
+
if @custom != nil
|
128
|
+
hash['custom'] = Statsig::HashUtils.sortHash(@custom)
|
129
|
+
end
|
130
|
+
if @statsig_environment != nil
|
131
|
+
hash['statsigEnvironment'] = @statsig_environment.clone.sort_by { |key| key }.to_h
|
132
|
+
end
|
133
|
+
if @private_attributes != nil
|
134
|
+
hash['privateAttributes'] = Statsig::HashUtils.sortHash(@private_attributes)
|
135
|
+
end
|
136
|
+
custom_ids = {}
|
137
|
+
if @custom_ids != nil
|
138
|
+
custom_ids = @custom_ids.clone
|
139
|
+
if custom_ids.key?("stableID")
|
140
|
+
custom_ids.delete("stableID")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
hash['customIDs'] = custom_ids.sort_by { |key| key }.to_h
|
144
|
+
return Statsig::HashUtils.djb2ForHash(hash.sort_by { |key| key }.to_h)
|
145
|
+
end
|
146
|
+
|
106
147
|
def value_lookup
|
107
148
|
{
|
108
149
|
'userID' => @user_id,
|
@@ -123,6 +164,15 @@ class StatsigUser
|
|
123
164
|
}
|
124
165
|
end
|
125
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
|
+
|
126
176
|
private
|
127
177
|
|
128
178
|
sig {
|
@@ -146,5 +196,4 @@ class StatsigUser
|
|
146
196
|
|
147
197
|
nil
|
148
198
|
end
|
149
|
-
|
150
|
-
end
|
199
|
+
end
|
@@ -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.pre.beta.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Statsig, Inc
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-11-11 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,11 +346,12 @@ 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
|
351
353
|
metadata: {}
|
352
|
-
post_install_message:
|
354
|
+
post_install_message:
|
353
355
|
rdoc_options: []
|
354
356
|
require_paths:
|
355
357
|
- lib
|
@@ -360,12 +362,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
360
362
|
version: 2.5.0
|
361
363
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
362
364
|
requirements:
|
363
|
-
- - "
|
365
|
+
- - ">"
|
364
366
|
- !ruby/object:Gem::Version
|
365
|
-
version:
|
367
|
+
version: 1.3.1
|
366
368
|
requirements: []
|
367
|
-
rubygems_version: 3.
|
368
|
-
signing_key:
|
369
|
+
rubygems_version: 3.3.7
|
370
|
+
signing_key:
|
369
371
|
specification_version: 4
|
370
372
|
summary: Statsig server SDK for Ruby
|
371
373
|
test_files: []
|