statsig 1.33.2 → 1.33.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/api_config.rb +38 -23
- data/lib/client_initialize_helpers.rb +1 -0
- data/lib/diagnostics.rb +6 -1
- data/lib/error_boundary.rb +3 -3
- data/lib/evaluation_helpers.rb +6 -14
- data/lib/evaluator.rb +16 -7
- data/lib/hash_utils.rb +4 -0
- data/lib/memo.rb +23 -0
- data/lib/spec_store.rb +4 -4
- data/lib/statsig.rb +8 -1
- data/lib/statsig_driver.rb +124 -103
- data/lib/statsig_event.rb +3 -1
- data/lib/statsig_logger.rb +20 -12
- data/lib/statsig_user.rb +94 -11
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b2947b68bfada4686360dc5aab60a460129a3ca5e4c63d6f2107af68788d2a8
|
|
4
|
+
data.tar.gz: 98e56828d1f31ec18afc0a2f46dbcab90c2a976dc95ef1db9ead5060b240952f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1f11434fb5ec030500429cdfd11146fe845568498cded4917e1f412ca50a86beb03f937febcaeceaf8523dacaa550b9024384dc58b0b3c9b2619d37041e0df0
|
|
7
|
+
data.tar.gz: d64fd8980ca648626ecdcd0095d4964aa531d24d7acaa7c07dd70013a32d0f7c5c548602e9578044c0567c0c2ac948f5dd8bd499c2178323b0ae176d0925a82c
|
data/lib/api_config.rb
CHANGED
|
@@ -87,39 +87,54 @@ module Statsig
|
|
|
87
87
|
|
|
88
88
|
class APICondition
|
|
89
89
|
|
|
90
|
-
attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type
|
|
91
|
-
|
|
90
|
+
attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type, :hash
|
|
92
91
|
def self.from_json(json)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
operator = operator
|
|
96
|
-
unless
|
|
97
|
-
|
|
92
|
+
hash = Statsig::HashUtils.md5(json.to_s).to_sym
|
|
93
|
+
return Statsig::Memo.for_global(:api_condition_from_json, hash) do
|
|
94
|
+
operator = json[:operator]
|
|
95
|
+
unless operator.nil?
|
|
96
|
+
operator = operator&.downcase&.to_sym
|
|
97
|
+
unless Const::SUPPORTED_OPERATORS.include?(operator)
|
|
98
|
+
raise UnsupportedConfigException
|
|
99
|
+
end
|
|
98
100
|
end
|
|
99
|
-
end
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
type = json[:type]
|
|
103
|
+
unless type.nil?
|
|
104
|
+
type = type&.downcase&.to_sym
|
|
105
|
+
unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
|
|
106
|
+
raise UnsupportedConfigException
|
|
107
|
+
end
|
|
106
108
|
end
|
|
107
|
-
end
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
new(
|
|
111
|
+
type: json[:type],
|
|
112
|
+
target_value: json[:targetValue],
|
|
113
|
+
operator: json[:operator],
|
|
114
|
+
field: json[:field],
|
|
115
|
+
additional_values: json[:additionalValues],
|
|
116
|
+
id_type: json[:idType],
|
|
117
|
+
hash: hash
|
|
118
|
+
)
|
|
119
|
+
end
|
|
117
120
|
end
|
|
118
121
|
|
|
119
122
|
private
|
|
120
123
|
|
|
121
|
-
def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:)
|
|
124
|
+
def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:, hash:)
|
|
125
|
+
@hash = hash
|
|
126
|
+
|
|
122
127
|
@type = type.to_sym unless type.nil?
|
|
128
|
+
if operator == "any_case_sensitive" || operator == "none_case_sensitive"
|
|
129
|
+
if target_value.is_a?(Array)
|
|
130
|
+
target_value = target_value.map { |item| [item.to_s, true] }.to_h
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
if operator == "any" || operator == "none"
|
|
134
|
+
if target_value.is_a?(Array)
|
|
135
|
+
target_value = target_value.map { |item| [item.to_s.downcase, true] }.to_h
|
|
136
|
+
end
|
|
137
|
+
end
|
|
123
138
|
@target_value = target_value
|
|
124
139
|
@operator = operator.to_sym unless operator.nil?
|
|
125
140
|
@field = field
|
|
@@ -25,6 +25,7 @@ module Statsig
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
|
|
28
|
+
|
|
28
29
|
target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
|
|
29
30
|
config_target_apps = config_spec.target_app_ids
|
|
30
31
|
|
data/lib/diagnostics.rb
CHANGED
|
@@ -58,7 +58,12 @@ module Statsig
|
|
|
58
58
|
rand * 10_000 < rate_over_ten_thousand
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
API_CALL_KEYS =
|
|
61
|
+
API_CALL_KEYS = {
|
|
62
|
+
:check_gate => true,
|
|
63
|
+
:get_config => true,
|
|
64
|
+
:get_experiment => true,
|
|
65
|
+
:get_layer => true
|
|
66
|
+
}.freeze
|
|
62
67
|
|
|
63
68
|
class Tracker
|
|
64
69
|
def initialize(diagnostics, context, key, step, tags = {})
|
data/lib/error_boundary.rb
CHANGED
|
@@ -10,9 +10,9 @@ module Statsig
|
|
|
10
10
|
@seen = Set.new
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def capture(
|
|
13
|
+
def capture(recover: -> {}, caller: nil)
|
|
14
14
|
begin
|
|
15
|
-
res =
|
|
15
|
+
res = yield
|
|
16
16
|
rescue StandardError, SystemStackError => e
|
|
17
17
|
if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
|
|
18
18
|
raise e
|
|
@@ -20,7 +20,7 @@ module Statsig
|
|
|
20
20
|
|
|
21
21
|
puts '[Statsig]: An unexpected exception occurred.'
|
|
22
22
|
puts e.message
|
|
23
|
-
log_exception(e, tag: caller)
|
|
23
|
+
log_exception(e, tag: caller&.to_s)
|
|
24
24
|
res = recover.call
|
|
25
25
|
end
|
|
26
26
|
return res
|
data/lib/evaluation_helpers.rb
CHANGED
|
@@ -10,7 +10,6 @@ module EvaluationHelpers
|
|
|
10
10
|
def self.match_string_in_array(array, value, ignore_case, func)
|
|
11
11
|
str_value = value.to_s
|
|
12
12
|
str_value_downcased = nil
|
|
13
|
-
|
|
14
13
|
return false if array.nil?
|
|
15
14
|
|
|
16
15
|
return array.any? do |item|
|
|
@@ -26,22 +25,15 @@ module EvaluationHelpers
|
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
def self.equal_string_in_array(array, value, ignore_case)
|
|
29
|
-
str_value = value.to_s
|
|
30
|
-
str_value_downcased = nil
|
|
31
|
-
|
|
32
28
|
return false if array.nil?
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
item_str = item.to_s
|
|
37
|
-
|
|
38
|
-
next false unless item_str.length == str_value.length
|
|
39
|
-
|
|
40
|
-
return true if item_str == str_value
|
|
41
|
-
next false unless ignore_case
|
|
30
|
+
str_value = value.to_s
|
|
31
|
+
str_value_downcased = nil
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
if ignore_case
|
|
34
|
+
return array.has_key?(value.to_s.downcase)
|
|
35
|
+
else
|
|
36
|
+
return array.has_key?(value.to_s)
|
|
45
37
|
end
|
|
46
38
|
end
|
|
47
39
|
|
data/lib/evaluator.rb
CHANGED
|
@@ -209,8 +209,8 @@ module Statsig
|
|
|
209
209
|
|
|
210
210
|
if user.custom_ids.nil? == false
|
|
211
211
|
evaluated_keys[:customIDs] = user.custom_ids
|
|
212
|
-
end
|
|
213
|
-
|
|
212
|
+
end
|
|
213
|
+
|
|
214
214
|
{
|
|
215
215
|
feature_gates: Statsig::ResponseFormatter
|
|
216
216
|
.get_responses(@spec_store.gates, self, user, client_sdk_key, hash_algo, include_local_overrides: include_local_overrides),
|
|
@@ -331,8 +331,19 @@ module Statsig
|
|
|
331
331
|
def eval_rule(user, rule, end_result)
|
|
332
332
|
pass = true
|
|
333
333
|
i = 0
|
|
334
|
+
|
|
335
|
+
memo = user.get_memo
|
|
334
336
|
until i >= rule.conditions.length
|
|
335
|
-
|
|
337
|
+
|
|
338
|
+
condition = rule.conditions[i]
|
|
339
|
+
|
|
340
|
+
if condition.type == :fail_gate || condition.type == :pass_gate
|
|
341
|
+
result = eval_condition(user, condition, end_result)
|
|
342
|
+
else
|
|
343
|
+
result = Memo.for(memo, :eval_rule, condition.hash) do
|
|
344
|
+
eval_condition(user, condition, end_result)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
336
347
|
|
|
337
348
|
pass = false if result != true
|
|
338
349
|
i += 1
|
|
@@ -370,7 +381,6 @@ module Statsig
|
|
|
370
381
|
return true
|
|
371
382
|
when :fail_gate, :pass_gate
|
|
372
383
|
check_gate(user, target, end_result)
|
|
373
|
-
|
|
374
384
|
gate_value = end_result.gate_value
|
|
375
385
|
|
|
376
386
|
unless end_result.disable_exposures
|
|
@@ -527,8 +537,7 @@ module Statsig
|
|
|
527
537
|
def get_value_from_user(user, field)
|
|
528
538
|
return nil unless field.is_a?(String)
|
|
529
539
|
|
|
530
|
-
value = get_value_from_user_field(user, field)
|
|
531
|
-
value ||= get_value_from_user_field(user, field.downcase)
|
|
540
|
+
value = get_value_from_user_field(user, field.downcase)
|
|
532
541
|
|
|
533
542
|
if value.nil?
|
|
534
543
|
value = user.custom[field] if user.custom.is_a?(Hash)
|
|
@@ -584,8 +593,8 @@ module Statsig
|
|
|
584
593
|
return nil unless field.is_a?(String)
|
|
585
594
|
|
|
586
595
|
ua = get_value_from_user(user, Const::USER_AGENT)
|
|
587
|
-
return nil unless ua.is_a?(String)
|
|
588
596
|
|
|
597
|
+
return nil unless ua.is_a?(String)
|
|
589
598
|
case field.downcase
|
|
590
599
|
when Const::OSNAME, Const::OS_NAME
|
|
591
600
|
os = UAParser.parse_os(ua)
|
data/lib/hash_utils.rb
CHANGED
|
@@ -19,6 +19,10 @@ module Statsig
|
|
|
19
19
|
return Digest::SHA256.base64digest(input_str)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def self.md5(input_str)
|
|
23
|
+
return Digest::MD5.base64digest(input_str)
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
def self.sortHash(input_hash)
|
|
23
27
|
dictionary = input_hash.clone.sort_by { |key| key }.to_h;
|
|
24
28
|
input_hash.each do |key, value|
|
data/lib/memo.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Statsig
|
|
2
|
+
class Memo
|
|
3
|
+
|
|
4
|
+
@global_memo = {}
|
|
5
|
+
|
|
6
|
+
def self.for(hash, method, key)
|
|
7
|
+
method_hash = hash[method]
|
|
8
|
+
unless method_hash
|
|
9
|
+
method_hash = hash[method] = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
return method_hash[key] if method_hash.key?(key)
|
|
13
|
+
|
|
14
|
+
method_hash[key] = yield
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.for_global(method, key)
|
|
18
|
+
return self.for(@global_memo, method, key) do
|
|
19
|
+
yield
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/spec_store.rb
CHANGED
|
@@ -218,12 +218,12 @@ module Statsig
|
|
|
218
218
|
end
|
|
219
219
|
|
|
220
220
|
Thread.new do
|
|
221
|
-
@error_boundary.capture(
|
|
221
|
+
@error_boundary.capture() do
|
|
222
222
|
loop do
|
|
223
223
|
sleep @options.rulesets_sync_interval
|
|
224
224
|
sync_config_specs
|
|
225
225
|
end
|
|
226
|
-
|
|
226
|
+
end
|
|
227
227
|
end
|
|
228
228
|
end
|
|
229
229
|
|
|
@@ -233,12 +233,12 @@ module Statsig
|
|
|
233
233
|
end
|
|
234
234
|
|
|
235
235
|
Thread.new do
|
|
236
|
-
@error_boundary.capture(
|
|
236
|
+
@error_boundary.capture() do
|
|
237
237
|
loop do
|
|
238
238
|
sleep @id_lists_sync_interval
|
|
239
239
|
sync_id_lists
|
|
240
240
|
end
|
|
241
|
-
|
|
241
|
+
end
|
|
242
242
|
end
|
|
243
243
|
end
|
|
244
244
|
|
data/lib/statsig.rb
CHANGED
|
@@ -331,6 +331,13 @@ module Statsig
|
|
|
331
331
|
@shared_instance&.clear_config_overrides
|
|
332
332
|
end
|
|
333
333
|
|
|
334
|
+
##
|
|
335
|
+
# @param [HashTable] debug information log with exposure events
|
|
336
|
+
def self.set_debug_info(debug_info)
|
|
337
|
+
ensure_initialized
|
|
338
|
+
@shared_instance&.set_debug_info(debug_info)
|
|
339
|
+
end
|
|
340
|
+
|
|
334
341
|
##
|
|
335
342
|
# Gets all evaluated values for the given user.
|
|
336
343
|
# These values can then be given to a Statsig Client SDK via bootstrapping.
|
|
@@ -356,7 +363,7 @@ module Statsig
|
|
|
356
363
|
def self.get_statsig_metadata
|
|
357
364
|
{
|
|
358
365
|
'sdkType' => 'ruby-server',
|
|
359
|
-
'sdkVersion' => '1.33.
|
|
366
|
+
'sdkVersion' => '1.33.4',
|
|
360
367
|
'languageVersion' => RUBY_VERSION
|
|
361
368
|
}
|
|
362
369
|
end
|
data/lib/statsig_driver.rb
CHANGED
|
@@ -11,7 +11,7 @@ require 'dynamic_config'
|
|
|
11
11
|
require 'feature_gate'
|
|
12
12
|
require 'error_boundary'
|
|
13
13
|
require 'layer'
|
|
14
|
-
|
|
14
|
+
require 'memo'
|
|
15
15
|
require 'diagnostics'
|
|
16
16
|
|
|
17
17
|
class StatsigDriver
|
|
@@ -26,7 +26,7 @@ class StatsigDriver
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
@err_boundary = Statsig::ErrorBoundary.new(secret_key)
|
|
29
|
-
@err_boundary.capture(
|
|
29
|
+
@err_boundary.capture(caller: __method__) do
|
|
30
30
|
@diagnostics = Statsig::Diagnostics.new()
|
|
31
31
|
tracker = @diagnostics.track('initialize', 'overall')
|
|
32
32
|
@options = options || StatsigOptions.new
|
|
@@ -40,7 +40,7 @@ class StatsigDriver
|
|
|
40
40
|
tracker.end(success: true)
|
|
41
41
|
|
|
42
42
|
@logger.log_diagnostics_event(@diagnostics, 'initialize')
|
|
43
|
-
|
|
43
|
+
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def get_gate_impl(
|
|
@@ -56,34 +56,38 @@ class StatsigDriver
|
|
|
56
56
|
return FeatureGate.new(gate_name) if gate.nil?
|
|
57
57
|
return FeatureGate.new(gate.name, target_app_ids: gate.target_app_ids)
|
|
58
58
|
end
|
|
59
|
+
|
|
59
60
|
user = verify_inputs(user, gate_name, 'gate_name')
|
|
61
|
+
return Statsig::Memo.for(user.get_memo(), :get_gate_impl, gate_name) do
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
res = Statsig::ConfigResult.new(name: gate_name, disable_exposures: disable_log_exposure, disable_evaluation_details: disable_evaluation_details)
|
|
64
|
+
@evaluator.check_gate(user, gate_name, res, ignore_local_overrides: ignore_local_overrides)
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
unless disable_log_exposure
|
|
65
67
|
@logger.log_gate_exposure(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
FeatureGate.from_config_result(res)
|
|
68
72
|
end
|
|
69
|
-
FeatureGate.from_config_result(res)
|
|
70
73
|
end
|
|
71
74
|
|
|
75
|
+
|
|
72
76
|
def get_gate(user, gate_name, options = nil)
|
|
73
|
-
@err_boundary.capture(
|
|
74
|
-
run_with_diagnostics(
|
|
77
|
+
@err_boundary.capture(caller: __method__, recover: -> {false}) do
|
|
78
|
+
run_with_diagnostics(caller: :get_gate) do
|
|
75
79
|
get_gate_impl(user, gate_name,
|
|
76
80
|
disable_log_exposure: options&.disable_log_exposure == true,
|
|
77
81
|
skip_evaluation: options&.skip_evaluation == true,
|
|
78
82
|
disable_evaluation_details: options&.disable_evaluation_details == true
|
|
79
83
|
)
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
end
|
|
85
|
+
end
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
def check_gate(user, gate_name, options = nil)
|
|
85
|
-
@err_boundary.capture(
|
|
86
|
-
run_with_diagnostics(
|
|
89
|
+
@err_boundary.capture(caller: __method__, recover: -> {false}) do
|
|
90
|
+
run_with_diagnostics(caller: :check_gate) do
|
|
87
91
|
get_gate_impl(
|
|
88
92
|
user,
|
|
89
93
|
gate_name,
|
|
@@ -91,22 +95,22 @@ class StatsigDriver
|
|
|
91
95
|
disable_evaluation_details: options&.disable_evaluation_details == true,
|
|
92
96
|
ignore_local_overrides: options&.ignore_local_overrides == true
|
|
93
97
|
).value
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
end
|
|
99
|
+
end
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
def manually_log_gate_exposure(user, gate_name)
|
|
99
|
-
@err_boundary.capture(
|
|
103
|
+
@err_boundary.capture(caller: __method__) do
|
|
100
104
|
res = Statsig::ConfigResult.new(name: gate_name)
|
|
101
105
|
@evaluator.check_gate(user, gate_name, res)
|
|
102
106
|
context = { :is_manual_exposure => true }
|
|
103
107
|
@logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
|
104
|
-
|
|
108
|
+
end
|
|
105
109
|
end
|
|
106
110
|
|
|
107
111
|
def get_config(user, dynamic_config_name, options = nil)
|
|
108
|
-
@err_boundary.capture(
|
|
109
|
-
run_with_diagnostics(
|
|
112
|
+
@err_boundary.capture(caller: __method__, recover: -> { DynamicConfig.new(dynamic_config_name) }) do
|
|
113
|
+
run_with_diagnostics(caller: :get_config) do
|
|
110
114
|
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
|
111
115
|
get_config_impl(
|
|
112
116
|
user,
|
|
@@ -115,13 +119,13 @@ class StatsigDriver
|
|
|
115
119
|
disable_evaluation_details: options&.disable_evaluation_details == true,
|
|
116
120
|
ignore_local_overrides: options&.ignore_local_overrides == true
|
|
117
121
|
)
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
end
|
|
123
|
+
end
|
|
120
124
|
end
|
|
121
125
|
|
|
122
126
|
def get_experiment(user, experiment_name, options = nil)
|
|
123
|
-
@err_boundary.capture(
|
|
124
|
-
run_with_diagnostics(
|
|
127
|
+
@err_boundary.capture(caller: __method__, recover: -> { DynamicConfig.new(experiment_name) }) do
|
|
128
|
+
run_with_diagnostics(caller: :get_experiment) do
|
|
125
129
|
user = verify_inputs(user, experiment_name, "experiment_name")
|
|
126
130
|
get_config_impl(
|
|
127
131
|
user,
|
|
@@ -131,63 +135,65 @@ class StatsigDriver
|
|
|
131
135
|
disable_evaluation_details: options&.disable_evaluation_details == true,
|
|
132
136
|
ignore_local_overrides: options&.ignore_local_overrides == true
|
|
133
137
|
)
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
end
|
|
139
|
+
end
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
def manually_log_config_exposure(user, config_name)
|
|
139
|
-
@err_boundary.capture(
|
|
143
|
+
@err_boundary.capture(caller: __method__) do
|
|
140
144
|
res = Statsig::ConfigResult.new(name: config_name)
|
|
141
145
|
@evaluator.get_config(user, config_name, res)
|
|
142
146
|
|
|
143
147
|
context = { :is_manual_exposure => true }
|
|
144
148
|
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
|
145
|
-
|
|
149
|
+
end
|
|
146
150
|
end
|
|
147
151
|
|
|
148
152
|
def get_user_persisted_values(user, id_type)
|
|
149
|
-
@err_boundary.capture(
|
|
153
|
+
@err_boundary.capture(caller: __method__,) do
|
|
150
154
|
persisted_values = @persistent_storage_utils.get_user_persisted_values(user, id_type)
|
|
151
155
|
return {} if persisted_values.nil?
|
|
152
156
|
|
|
153
157
|
persisted_values
|
|
154
|
-
|
|
158
|
+
end
|
|
155
159
|
end
|
|
156
160
|
|
|
157
161
|
def get_layer(user, layer_name, options = nil)
|
|
158
|
-
@err_boundary.capture(
|
|
159
|
-
run_with_diagnostics(
|
|
162
|
+
@err_boundary.capture(caller: __method__, recover: -> { Layer.new(layer_name) }) do
|
|
163
|
+
run_with_diagnostics(caller: :get_layer) do
|
|
160
164
|
user = verify_inputs(user, layer_name, "layer_name")
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
165
|
+
Statsig::Memo.for(user.get_memo(), :get_layer, layer_name) do
|
|
166
|
+
exposures_disabled = options&.disable_log_exposure == true
|
|
167
|
+
res = Statsig::ConfigResult.new(
|
|
168
|
+
name: layer_name,
|
|
169
|
+
disable_exposures: exposures_disabled,
|
|
170
|
+
disable_evaluation_details: options&.disable_evaluation_details == true
|
|
171
|
+
)
|
|
172
|
+
@evaluator.get_layer(user, layer_name, res)
|
|
173
|
+
|
|
174
|
+
exposure_log_func = !exposures_disabled ? lambda { |layer, parameter_name|
|
|
175
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
|
176
|
+
} : nil
|
|
177
|
+
|
|
178
|
+
Layer.new(res.name, res.json_value, res.rule_id, res.group_name, res.config_delegate, exposure_log_func)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
176
182
|
end
|
|
177
183
|
|
|
178
184
|
def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
|
179
|
-
@err_boundary.capture(
|
|
185
|
+
@err_boundary.capture(caller: __method__) do
|
|
180
186
|
res = Statsig::ConfigResult.new(name: layer_name)
|
|
181
187
|
@evaluator.get_layer(user, layer_name, res)
|
|
182
188
|
|
|
183
189
|
layer = Layer.new(layer_name, res.json_value, res.rule_id, res.group_name, res.config_delegate)
|
|
184
190
|
context = { :is_manual_exposure => true }
|
|
185
191
|
@logger.log_layer_exposure(user, layer, parameter_name, res, context)
|
|
186
|
-
|
|
192
|
+
end
|
|
187
193
|
end
|
|
188
194
|
|
|
189
195
|
def log_event(user, event_name, value = nil, metadata = nil)
|
|
190
|
-
@err_boundary.capture(
|
|
196
|
+
@err_boundary.capture(caller: __method__) do
|
|
191
197
|
if !user.nil? && !user.instance_of?(StatsigUser)
|
|
192
198
|
raise Statsig::ValueError.new('Must provide a valid StatsigUser or nil')
|
|
193
199
|
end
|
|
@@ -200,93 +206,99 @@ class StatsigDriver
|
|
|
200
206
|
event.value = value
|
|
201
207
|
event.metadata = metadata
|
|
202
208
|
@logger.log_event(event)
|
|
203
|
-
|
|
209
|
+
end
|
|
204
210
|
end
|
|
205
211
|
|
|
206
212
|
def manually_sync_rulesets
|
|
207
|
-
@err_boundary.capture(
|
|
213
|
+
@err_boundary.capture(caller: __method__) do
|
|
208
214
|
@evaluator.spec_store.sync_config_specs
|
|
209
|
-
|
|
215
|
+
end
|
|
210
216
|
end
|
|
211
217
|
|
|
212
218
|
def manually_sync_idlists
|
|
213
|
-
@err_boundary.capture(
|
|
219
|
+
@err_boundary.capture(caller: __method__) do
|
|
214
220
|
@evaluator.spec_store.sync_id_lists
|
|
215
|
-
|
|
221
|
+
end
|
|
216
222
|
end
|
|
217
223
|
|
|
218
224
|
def list_gates
|
|
219
|
-
@err_boundary.capture(
|
|
225
|
+
@err_boundary.capture(caller: __method__) do
|
|
220
226
|
@evaluator.list_gates
|
|
221
|
-
|
|
227
|
+
end
|
|
222
228
|
end
|
|
223
229
|
|
|
224
230
|
def list_configs
|
|
225
|
-
@err_boundary.capture(
|
|
231
|
+
@err_boundary.capture(caller: __method__) do
|
|
226
232
|
@evaluator.list_configs
|
|
227
|
-
|
|
233
|
+
end
|
|
228
234
|
end
|
|
229
235
|
|
|
230
236
|
def list_experiments
|
|
231
|
-
@err_boundary.capture(
|
|
237
|
+
@err_boundary.capture(caller: __method__) do
|
|
232
238
|
@evaluator.list_experiments
|
|
233
|
-
|
|
239
|
+
end
|
|
234
240
|
end
|
|
235
241
|
|
|
236
242
|
def list_autotunes
|
|
237
|
-
@err_boundary.capture(
|
|
243
|
+
@err_boundary.capture(caller: __method__) do
|
|
238
244
|
@evaluator.list_autotunes
|
|
239
|
-
|
|
245
|
+
end
|
|
240
246
|
end
|
|
241
247
|
|
|
242
248
|
def list_layers
|
|
243
|
-
@err_boundary.capture(
|
|
249
|
+
@err_boundary.capture(caller: __method__) do
|
|
244
250
|
@evaluator.list_layers
|
|
245
|
-
|
|
251
|
+
end
|
|
246
252
|
end
|
|
247
253
|
|
|
248
254
|
def shutdown
|
|
249
|
-
@err_boundary.capture(
|
|
255
|
+
@err_boundary.capture(caller: __method__) do
|
|
250
256
|
@shutdown = true
|
|
251
257
|
@logger.shutdown
|
|
252
258
|
@evaluator.shutdown
|
|
253
|
-
|
|
259
|
+
end
|
|
254
260
|
end
|
|
255
261
|
|
|
256
262
|
def override_gate(gate_name, gate_value)
|
|
257
|
-
@err_boundary.capture(
|
|
263
|
+
@err_boundary.capture(caller: __method__) do
|
|
258
264
|
@evaluator.override_gate(gate_name, gate_value)
|
|
259
|
-
|
|
265
|
+
end
|
|
260
266
|
end
|
|
261
267
|
|
|
262
268
|
def remove_gate_override(gate_name)
|
|
263
|
-
@err_boundary.capture(
|
|
269
|
+
@err_boundary.capture(caller: __method__) do
|
|
264
270
|
@evaluator.remove_gate_override(gate_name)
|
|
265
|
-
|
|
271
|
+
end
|
|
266
272
|
end
|
|
267
273
|
|
|
268
274
|
def clear_gate_overrides
|
|
269
|
-
@err_boundary.capture(
|
|
275
|
+
@err_boundary.capture(caller: __method__) do
|
|
270
276
|
@evaluator.clear_gate_overrides
|
|
271
|
-
|
|
277
|
+
end
|
|
272
278
|
end
|
|
273
279
|
|
|
274
280
|
def override_config(config_name, config_value)
|
|
275
|
-
@err_boundary.capture(
|
|
281
|
+
@err_boundary.capture(caller: __method__) do
|
|
276
282
|
@evaluator.override_config(config_name, config_value)
|
|
277
|
-
|
|
283
|
+
end
|
|
278
284
|
end
|
|
279
285
|
|
|
280
286
|
def remove_config_override(config_name)
|
|
281
|
-
@err_boundary.capture(
|
|
287
|
+
@err_boundary.capture(caller: __method__) do
|
|
282
288
|
@evaluator.remove_config_override(config_name)
|
|
283
|
-
|
|
289
|
+
end
|
|
284
290
|
end
|
|
285
291
|
|
|
286
292
|
def clear_config_overrides
|
|
287
|
-
@err_boundary.capture(
|
|
293
|
+
@err_boundary.capture(caller: __method__) do
|
|
288
294
|
@evaluator.clear_config_overrides
|
|
289
|
-
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def set_debug_info(debug_info)
|
|
299
|
+
@err_boundary.capture(caller: __method__) do
|
|
300
|
+
@logger.set_debug_info(debug_info)
|
|
301
|
+
end
|
|
290
302
|
end
|
|
291
303
|
|
|
292
304
|
# @param [StatsigUser] user
|
|
@@ -294,11 +306,11 @@ class StatsigDriver
|
|
|
294
306
|
# @param [Boolean] include_local_overrides
|
|
295
307
|
# @return [Hash]
|
|
296
308
|
def get_client_initialize_response(user, hash, client_sdk_key, include_local_overrides)
|
|
297
|
-
@err_boundary.capture(
|
|
309
|
+
@err_boundary.capture(caller: __method__, recover: -> { nil }) do
|
|
298
310
|
validate_user(user)
|
|
299
311
|
normalize_user(user)
|
|
300
312
|
@evaluator.get_client_initialize_response(user, hash, client_sdk_key, include_local_overrides)
|
|
301
|
-
|
|
313
|
+
end
|
|
302
314
|
end
|
|
303
315
|
|
|
304
316
|
def maybe_restart_background_threads
|
|
@@ -306,22 +318,24 @@ class StatsigDriver
|
|
|
306
318
|
return
|
|
307
319
|
end
|
|
308
320
|
|
|
309
|
-
@err_boundary.capture(
|
|
321
|
+
@err_boundary.capture(caller: __method__) do
|
|
310
322
|
@evaluator.maybe_restart_background_threads
|
|
311
323
|
@logger.maybe_restart_background_threads
|
|
312
|
-
|
|
324
|
+
end
|
|
313
325
|
end
|
|
314
326
|
|
|
315
327
|
private
|
|
316
328
|
|
|
317
|
-
def run_with_diagnostics(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
diagnostics = Statsig::Diagnostics.new()
|
|
321
|
-
tracker = diagnostics.track('api_call', caller)
|
|
329
|
+
def run_with_diagnostics(caller:)
|
|
330
|
+
if !Statsig::Diagnostics::API_CALL_KEYS[caller] || !Statsig::Diagnostics.sample(1)
|
|
331
|
+
return yield
|
|
322
332
|
end
|
|
333
|
+
|
|
334
|
+
diagnostics = Statsig::Diagnostics.new()
|
|
335
|
+
tracker = diagnostics.track('api_call', caller.to_s)
|
|
336
|
+
|
|
323
337
|
begin
|
|
324
|
-
res =
|
|
338
|
+
res = yield
|
|
325
339
|
tracker&.end(success: true)
|
|
326
340
|
rescue StandardError => e
|
|
327
341
|
tracker&.end(success: false)
|
|
@@ -334,28 +348,35 @@ class StatsigDriver
|
|
|
334
348
|
|
|
335
349
|
def verify_inputs(user, config_name, variable_name)
|
|
336
350
|
validate_user(user)
|
|
351
|
+
user = Statsig::Memo.for(user.get_memo(), :verify_inputs, 0) do
|
|
352
|
+
user = normalize_user(user)
|
|
353
|
+
check_shutdown
|
|
354
|
+
maybe_restart_background_threads
|
|
355
|
+
user
|
|
356
|
+
end
|
|
357
|
+
|
|
337
358
|
if !config_name.is_a?(String) || config_name.empty?
|
|
338
359
|
raise Statsig::ValueError.new("Invalid #{variable_name} provided")
|
|
339
360
|
end
|
|
340
361
|
|
|
341
|
-
|
|
342
|
-
maybe_restart_background_threads
|
|
343
|
-
normalize_user(user)
|
|
362
|
+
user
|
|
344
363
|
end
|
|
345
364
|
|
|
346
365
|
def get_config_impl(user, config_name, disable_log_exposure, user_persisted_values: nil, disable_evaluation_details: false, ignore_local_overrides: false)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
366
|
+
return Statsig::Memo.for(user.get_memo(), :get_config_impl, config_name) do
|
|
367
|
+
res = Statsig::ConfigResult.new(
|
|
368
|
+
name: config_name,
|
|
369
|
+
disable_exposures: disable_log_exposure,
|
|
370
|
+
disable_evaluation_details: disable_evaluation_details
|
|
371
|
+
)
|
|
372
|
+
@evaluator.get_config(user, config_name, res, user_persisted_values: user_persisted_values, ignore_local_overrides: ignore_local_overrides)
|
|
353
373
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
374
|
+
unless disable_log_exposure
|
|
375
|
+
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
|
376
|
+
end
|
|
357
377
|
|
|
358
|
-
|
|
378
|
+
DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type, res.evaluation_details)
|
|
379
|
+
end
|
|
359
380
|
end
|
|
360
381
|
|
|
361
382
|
def validate_user(user)
|
data/lib/statsig_event.rb
CHANGED
data/lib/statsig_logger.rb
CHANGED
|
@@ -28,6 +28,7 @@ module Statsig
|
|
|
28
28
|
@deduper = Concurrent::Set.new()
|
|
29
29
|
@interval = 0
|
|
30
30
|
@flush_mutex = Mutex.new
|
|
31
|
+
@debug_info = nil
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def log_event(event)
|
|
@@ -45,6 +46,9 @@ module Statsig
|
|
|
45
46
|
gateValue: value.to_s,
|
|
46
47
|
ruleID: rule_id || Statsig::Const::EMPTY_STR,
|
|
47
48
|
}
|
|
49
|
+
if @debug_info != nil
|
|
50
|
+
metadata[:debugInfo] = @debug_info
|
|
51
|
+
end
|
|
48
52
|
return false if not is_unique_exposure(user, $gate_exposure_event, metadata)
|
|
49
53
|
event.metadata = metadata
|
|
50
54
|
|
|
@@ -62,6 +66,9 @@ module Statsig
|
|
|
62
66
|
config: config_name,
|
|
63
67
|
ruleID: rule_id || Statsig::Const::EMPTY_STR,
|
|
64
68
|
}
|
|
69
|
+
if @debug_info != nil
|
|
70
|
+
metadata[:debugInfo] = @debug_info
|
|
71
|
+
end
|
|
65
72
|
return false if not is_unique_exposure(user, $config_exposure_event, metadata)
|
|
66
73
|
event.metadata = metadata
|
|
67
74
|
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
|
@@ -89,6 +96,9 @@ module Statsig
|
|
|
89
96
|
parameterName: parameter_name,
|
|
90
97
|
isExplicitParameter: String(is_explicit)
|
|
91
98
|
}
|
|
99
|
+
if @debug_info != nil
|
|
100
|
+
metadata[:debugInfo] = @debug_info
|
|
101
|
+
end
|
|
92
102
|
return false unless is_unique_exposure(user, $layer_exposure_event, metadata)
|
|
93
103
|
event.metadata = metadata
|
|
94
104
|
event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
|
|
@@ -117,14 +127,14 @@ module Statsig
|
|
|
117
127
|
|
|
118
128
|
def periodic_flush
|
|
119
129
|
Thread.new do
|
|
120
|
-
@error_boundary.capture(
|
|
130
|
+
@error_boundary.capture() do
|
|
121
131
|
loop do
|
|
122
132
|
sleep @options.logging_interval_seconds
|
|
123
133
|
flush_async
|
|
124
134
|
@interval += 1
|
|
125
135
|
@deduper.clear if @interval % 2 == 0
|
|
126
136
|
end
|
|
127
|
-
|
|
137
|
+
end
|
|
128
138
|
end
|
|
129
139
|
end
|
|
130
140
|
|
|
@@ -160,6 +170,10 @@ module Statsig
|
|
|
160
170
|
end
|
|
161
171
|
end
|
|
162
172
|
|
|
173
|
+
def set_debug_info(debug_info)
|
|
174
|
+
@debug_info = debug_info
|
|
175
|
+
end
|
|
176
|
+
|
|
163
177
|
private
|
|
164
178
|
|
|
165
179
|
def safe_add_eval_details(eval_details, event)
|
|
@@ -186,21 +200,15 @@ module Statsig
|
|
|
186
200
|
def is_unique_exposure(user, event_name, metadata)
|
|
187
201
|
return true if user.nil?
|
|
188
202
|
@deduper.clear if @deduper.size > 10000
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
custom_id_key = user.custom_ids.values.join(',')
|
|
192
|
-
end
|
|
203
|
+
|
|
204
|
+
user_key = user.user_key
|
|
193
205
|
|
|
194
206
|
metadata_key = ''
|
|
195
207
|
if metadata.is_a?(Hash)
|
|
196
208
|
metadata_key = metadata.reject { |key, _| $ignored_metadata_keys.include?(key) }.values.join(',')
|
|
197
209
|
end
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
unless user.user_id.nil?
|
|
201
|
-
user_id_key = user.user_id
|
|
202
|
-
end
|
|
203
|
-
key = [user_id_key, custom_id_key, event_name, metadata_key].join(',')
|
|
210
|
+
|
|
211
|
+
key = [user_key, event_name, metadata_key].join(',')
|
|
204
212
|
|
|
205
213
|
return false if @deduper.include?(key)
|
|
206
214
|
@deduper.add(key)
|
data/lib/statsig_user.rb
CHANGED
|
@@ -1,39 +1,78 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'constants'
|
|
3
|
-
|
|
4
3
|
##
|
|
5
4
|
# The user object to be evaluated against your Statsig configurations (gates/experiments/dynamic configs).
|
|
6
5
|
class StatsigUser
|
|
7
6
|
|
|
8
7
|
# An identifier for this user. Evaluated against the User ID criteria. (https://docs.statsig.com/feature-gates/conditions#userid)
|
|
9
|
-
|
|
8
|
+
attr_reader :user_id
|
|
9
|
+
def user_id=(value)
|
|
10
|
+
value_changed()
|
|
11
|
+
@user_id = value
|
|
12
|
+
end
|
|
10
13
|
|
|
11
14
|
# An identifier for this user. Evaluated against the Email criteria. (https://docs.statsig.com/feature-gates/conditions#email)
|
|
12
|
-
|
|
15
|
+
attr_reader :email
|
|
16
|
+
def email=(value)
|
|
17
|
+
value_changed()
|
|
18
|
+
@email = value
|
|
19
|
+
end
|
|
13
20
|
|
|
14
21
|
# An IP address associated with this user. Evaluated against the IP Address criteria. (https://docs.statsig.com/feature-gates/conditions#ip)
|
|
15
|
-
|
|
22
|
+
attr_reader :ip
|
|
23
|
+
def ip=(value)
|
|
24
|
+
value_changed()
|
|
25
|
+
@ip = value
|
|
26
|
+
end
|
|
16
27
|
|
|
17
28
|
# A user agent string associated with this user. Evaluated against Browser Version and Name (https://docs.statsig.com/feature-gates/conditions#browser-version)
|
|
18
|
-
|
|
29
|
+
attr_reader :user_agent
|
|
30
|
+
def user_agent=(value)
|
|
31
|
+
value_changed()
|
|
32
|
+
@user_agent = value
|
|
33
|
+
end
|
|
19
34
|
|
|
20
35
|
# The country code associated with this user (e.g New Zealand => NZ). Evaluated against the Country criteria. (https://docs.statsig.com/feature-gates/conditions#country)
|
|
21
|
-
|
|
36
|
+
attr_reader :country
|
|
37
|
+
def country=(value)
|
|
38
|
+
value_changed()
|
|
39
|
+
@country = value
|
|
40
|
+
end
|
|
22
41
|
|
|
23
42
|
# An locale for this user.
|
|
24
|
-
|
|
43
|
+
attr_reader :locale
|
|
44
|
+
def locale=(value)
|
|
45
|
+
value_changed()
|
|
46
|
+
@locale = value
|
|
47
|
+
end
|
|
25
48
|
|
|
26
49
|
# The current app version the user is interacting with. Evaluated against the App Version criteria. (https://docs.statsig.com/feature-gates/conditions#app-version)
|
|
27
|
-
|
|
50
|
+
attr_reader :app_version
|
|
51
|
+
def app_version=(value)
|
|
52
|
+
value_changed()
|
|
53
|
+
@app_version = value
|
|
54
|
+
end
|
|
28
55
|
|
|
29
56
|
# A Hash you can use to set environment variables that apply to this user. e.g. { "tier" => "development" }
|
|
30
|
-
|
|
57
|
+
attr_reader :statsig_environment
|
|
58
|
+
def statsig_environment=(value)
|
|
59
|
+
value_changed()
|
|
60
|
+
@statsig_environment = value
|
|
61
|
+
end
|
|
31
62
|
|
|
32
63
|
# Any Custom IDs to associated with the user. (See https://docs.statsig.com/guides/experiment-on-custom-id-types)
|
|
33
|
-
|
|
64
|
+
attr_reader :custom_ids
|
|
65
|
+
def custom_ids=(value)
|
|
66
|
+
value_changed()
|
|
67
|
+
@custom_ids = value
|
|
68
|
+
end
|
|
34
69
|
|
|
35
70
|
# Any value you wish to use in evaluation, but do not want logged with events, can be stored in this field.
|
|
36
|
-
|
|
71
|
+
attr_reader :private_attributes
|
|
72
|
+
def private_attributes=(value)
|
|
73
|
+
value_changed()
|
|
74
|
+
@private_attributes = value
|
|
75
|
+
end
|
|
37
76
|
|
|
38
77
|
def custom
|
|
39
78
|
@custom
|
|
@@ -41,9 +80,12 @@ class StatsigUser
|
|
|
41
80
|
|
|
42
81
|
# Any custom fields for this user. Evaluated against the Custom criteria. (https://docs.statsig.com/feature-gates/conditions#custom)
|
|
43
82
|
def custom=(value)
|
|
83
|
+
value_changed()
|
|
44
84
|
@custom = value.is_a?(Hash) ? value : Hash.new
|
|
45
85
|
end
|
|
46
86
|
|
|
87
|
+
attr_accessor :memo_timeout
|
|
88
|
+
|
|
47
89
|
def initialize(user_hash)
|
|
48
90
|
the_hash = user_hash
|
|
49
91
|
begin
|
|
@@ -63,6 +105,9 @@ class StatsigUser
|
|
|
63
105
|
@private_attributes = from_hash(the_hash, [:private_attributes, :privateAttributes], Hash)
|
|
64
106
|
@custom_ids = from_hash(the_hash, [:custom_ids, :customIDs], Hash)
|
|
65
107
|
@statsig_environment = from_hash(the_hash, [:statsig_environment, :statsigEnvironment], Hash)
|
|
108
|
+
@memo = {}
|
|
109
|
+
@dirty = true
|
|
110
|
+
@memo_timeout = 2
|
|
66
111
|
end
|
|
67
112
|
|
|
68
113
|
def serialize(for_logging)
|
|
@@ -138,7 +183,45 @@ class StatsigUser
|
|
|
138
183
|
@user_id
|
|
139
184
|
end
|
|
140
185
|
|
|
186
|
+
def get_memo
|
|
187
|
+
current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
188
|
+
|
|
189
|
+
if @dirty || current_time - (@memo_access_time ||= current_time) > @memo_timeout
|
|
190
|
+
if @memo.size() > 0
|
|
191
|
+
@memo.clear
|
|
192
|
+
end
|
|
193
|
+
@dirty = false
|
|
194
|
+
@memo_access_time = current_time
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
@memo
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def clear_memo
|
|
201
|
+
@memo.clear
|
|
202
|
+
@dirty = false
|
|
203
|
+
@memo_access_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def user_key
|
|
207
|
+
unless !@dirty && defined? @_user_key
|
|
208
|
+
custom_id_key = ''
|
|
209
|
+
if self.custom_ids.is_a?(Hash)
|
|
210
|
+
custom_id_key = self.custom_ids.values.join(',')
|
|
211
|
+
end
|
|
212
|
+
user_id_key = ''
|
|
213
|
+
unless self.user_id.nil?
|
|
214
|
+
user_id_key = self.user_id.to_s
|
|
215
|
+
end
|
|
216
|
+
@_user_key = user_id_key + ',' + custom_id_key.to_s
|
|
217
|
+
end
|
|
218
|
+
@_user_key
|
|
219
|
+
end
|
|
220
|
+
|
|
141
221
|
private
|
|
222
|
+
def value_changed
|
|
223
|
+
@dirty = true
|
|
224
|
+
end
|
|
142
225
|
|
|
143
226
|
# Pulls fields from the user hash via Symbols and Strings
|
|
144
227
|
def from_hash(user_hash, keys, type)
|
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.33.
|
|
4
|
+
version: 1.33.4
|
|
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-03-
|
|
11
|
+
date: 2024-03-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -310,6 +310,7 @@ files:
|
|
|
310
310
|
- lib/interfaces/data_store.rb
|
|
311
311
|
- lib/interfaces/user_persistent_storage.rb
|
|
312
312
|
- lib/layer.rb
|
|
313
|
+
- lib/memo.rb
|
|
313
314
|
- lib/network.rb
|
|
314
315
|
- lib/spec_store.rb
|
|
315
316
|
- lib/statsig.rb
|