statsig 1.25.2 → 1.33.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/api_config.rb +128 -0
- data/lib/client_initialize_helpers.rb +110 -82
- data/lib/config_result.rb +42 -11
- data/lib/constants.rb +60 -0
- data/lib/diagnostics.rb +48 -70
- data/lib/dynamic_config.rb +5 -15
- data/lib/error_boundary.rb +32 -49
- data/lib/evaluation_details.rb +17 -8
- data/lib/evaluation_helpers.rb +35 -3
- data/lib/evaluator.rb +425 -300
- data/lib/feature_gate.rb +46 -0
- data/lib/hash_utils.rb +32 -0
- data/lib/id_list.rb +2 -2
- data/lib/interfaces/data_store.rb +1 -1
- data/lib/interfaces/user_persistent_storage.rb +12 -0
- data/lib/layer.rb +7 -12
- data/lib/network.rb +57 -55
- data/lib/spec_store.rb +213 -130
- data/lib/statsig.rb +186 -82
- data/lib/statsig_driver.rb +227 -147
- data/lib/statsig_errors.rb +7 -0
- data/lib/statsig_event.rb +8 -8
- data/lib/statsig_logger.rb +54 -42
- data/lib/statsig_options.rb +23 -49
- data/lib/statsig_user.rb +65 -57
- data/lib/ua_parser.rb +1 -0
- data/lib/uri_helper.rb +2 -10
- data/lib/user_persistent_storage_utils.rb +89 -0
- metadata +46 -20
data/lib/statsig_logger.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require 'constants'
|
2
2
|
require 'statsig_event'
|
3
3
|
require 'concurrent-ruby'
|
4
4
|
|
@@ -6,10 +6,10 @@ $gate_exposure_event = 'statsig::gate_exposure'
|
|
6
6
|
$config_exposure_event = 'statsig::config_exposure'
|
7
7
|
$layer_exposure_event = 'statsig::layer_exposure'
|
8
8
|
$diagnostics_event = 'statsig::diagnostics'
|
9
|
-
$ignored_metadata_keys = [
|
9
|
+
$ignored_metadata_keys = [:serverTime, :configSyncTime, :initTime, :reason]
|
10
10
|
module Statsig
|
11
11
|
class StatsigLogger
|
12
|
-
def initialize(network, options)
|
12
|
+
def initialize(network, options, error_boundary)
|
13
13
|
@network = network
|
14
14
|
@events = []
|
15
15
|
@options = options
|
@@ -23,9 +23,11 @@ module Statsig
|
|
23
23
|
fallback_policy: :discard
|
24
24
|
)
|
25
25
|
|
26
|
+
@error_boundary = error_boundary
|
26
27
|
@background_flush = periodic_flush
|
27
28
|
@deduper = Concurrent::Set.new()
|
28
29
|
@interval = 0
|
30
|
+
@flush_mutex = Mutex.new
|
29
31
|
end
|
30
32
|
|
31
33
|
def log_event(event)
|
@@ -39,9 +41,9 @@ module Statsig
|
|
39
41
|
event = StatsigEvent.new($gate_exposure_event)
|
40
42
|
event.user = user
|
41
43
|
metadata = {
|
42
|
-
|
43
|
-
|
44
|
-
|
44
|
+
gate: gate_name,
|
45
|
+
gateValue: value.to_s,
|
46
|
+
ruleID: rule_id || Statsig::Const::EMPTY_STR,
|
45
47
|
}
|
46
48
|
return false if not is_unique_exposure(user, $gate_exposure_event, metadata)
|
47
49
|
event.metadata = metadata
|
@@ -57,8 +59,8 @@ module Statsig
|
|
57
59
|
event = StatsigEvent.new($config_exposure_event)
|
58
60
|
event.user = user
|
59
61
|
metadata = {
|
60
|
-
|
61
|
-
|
62
|
+
config: config_name,
|
63
|
+
ruleID: rule_id || Statsig::Const::EMPTY_STR,
|
62
64
|
}
|
63
65
|
return false if not is_unique_exposure(user, $config_exposure_event, metadata)
|
64
66
|
event.metadata = metadata
|
@@ -70,8 +72,8 @@ module Statsig
|
|
70
72
|
end
|
71
73
|
|
72
74
|
def log_layer_exposure(user, layer, parameter_name, config_evaluation, context = nil)
|
73
|
-
exposures = config_evaluation.undelegated_sec_exps
|
74
|
-
allocated_experiment =
|
75
|
+
exposures = config_evaluation.undelegated_sec_exps || []
|
76
|
+
allocated_experiment = Statsig::Const::EMPTY_STR
|
75
77
|
is_explicit = (config_evaluation.explicit_parameters&.include? parameter_name) || false
|
76
78
|
if is_explicit
|
77
79
|
allocated_experiment = config_evaluation.config_delegate
|
@@ -81,13 +83,13 @@ module Statsig
|
|
81
83
|
event = StatsigEvent.new($layer_exposure_event)
|
82
84
|
event.user = user
|
83
85
|
metadata = {
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
86
|
+
config: layer.name,
|
87
|
+
ruleID: layer.rule_id || Statsig::Const::EMPTY_STR,
|
88
|
+
allocatedExperiment: allocated_experiment,
|
89
|
+
parameterName: parameter_name,
|
90
|
+
isExplicitParameter: String(is_explicit)
|
89
91
|
}
|
90
|
-
return false
|
92
|
+
return false unless is_unique_exposure(user, $layer_exposure_event, metadata)
|
91
93
|
event.metadata = metadata
|
92
94
|
event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
|
93
95
|
|
@@ -96,25 +98,33 @@ module Statsig
|
|
96
98
|
log_event(event)
|
97
99
|
end
|
98
100
|
|
99
|
-
def log_diagnostics_event(diagnostics, user = nil)
|
100
|
-
return if
|
101
|
-
|
101
|
+
def log_diagnostics_event(diagnostics, context, user = nil)
|
102
|
+
return if diagnostics.nil?
|
103
|
+
if @options.disable_diagnostics_logging
|
104
|
+
diagnostics.clear_markers(context)
|
105
|
+
return
|
106
|
+
end
|
102
107
|
|
103
108
|
event = StatsigEvent.new($diagnostics_event)
|
104
109
|
event.user = user
|
105
|
-
|
110
|
+
serialized = diagnostics.serialize_with_sampling(context)
|
111
|
+
diagnostics.clear_markers(context)
|
112
|
+
return if serialized[:markers].empty?
|
113
|
+
|
114
|
+
event.metadata = serialized
|
106
115
|
log_event(event)
|
107
|
-
diagnostics.clear_markers
|
108
116
|
end
|
109
117
|
|
110
118
|
def periodic_flush
|
111
119
|
Thread.new do
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
120
|
+
@error_boundary.capture(task: lambda {
|
121
|
+
loop do
|
122
|
+
sleep @options.logging_interval_seconds
|
123
|
+
flush_async
|
124
|
+
@interval += 1
|
125
|
+
@deduper.clear if @interval % 2 == 0
|
126
|
+
end
|
127
|
+
})
|
118
128
|
end
|
119
129
|
end
|
120
130
|
|
@@ -132,18 +142,20 @@ module Statsig
|
|
132
142
|
end
|
133
143
|
|
134
144
|
def flush
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
@events = []
|
140
|
-
flush_events = events_clone.map { |e| e.serialize }
|
145
|
+
@flush_mutex.synchronize do
|
146
|
+
if @events.length.zero?
|
147
|
+
return
|
148
|
+
end
|
141
149
|
|
142
|
-
|
150
|
+
events_clone = @events
|
151
|
+
@events = []
|
152
|
+
flush_events = events_clone.map { |e| e.serialize }
|
153
|
+
@network.post_logs(flush_events)
|
154
|
+
end
|
143
155
|
end
|
144
156
|
|
145
157
|
def maybe_restart_background_threads
|
146
|
-
if @background_flush.nil?
|
158
|
+
if @background_flush.nil? || !@background_flush.alive?
|
147
159
|
@background_flush = periodic_flush
|
148
160
|
end
|
149
161
|
end
|
@@ -155,10 +167,10 @@ module Statsig
|
|
155
167
|
return
|
156
168
|
end
|
157
169
|
|
158
|
-
event.metadata[
|
159
|
-
event.metadata[
|
160
|
-
event.metadata[
|
161
|
-
event.metadata[
|
170
|
+
event.metadata[:reason] = eval_details.reason
|
171
|
+
event.metadata[:configSyncTime] = eval_details.config_sync_time
|
172
|
+
event.metadata[:initTime] = eval_details.init_time
|
173
|
+
event.metadata[:serverTime] = eval_details.server_time
|
162
174
|
end
|
163
175
|
|
164
176
|
def safe_add_exposure_context(context, event)
|
@@ -166,8 +178,8 @@ module Statsig
|
|
166
178
|
return
|
167
179
|
end
|
168
180
|
|
169
|
-
if context[
|
170
|
-
event.metadata[
|
181
|
+
if context[:is_manual_exposure]
|
182
|
+
event.metadata[:isManualExposure] = 'true'
|
171
183
|
end
|
172
184
|
end
|
173
185
|
|
@@ -195,4 +207,4 @@ module Statsig
|
|
195
207
|
true
|
196
208
|
end
|
197
209
|
end
|
198
|
-
end
|
210
|
+
end
|
data/lib/statsig_options.rb
CHANGED
@@ -1,132 +1,100 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
1
|
require_relative 'interfaces/data_store'
|
2
|
+
require_relative 'interfaces/user_persistent_storage'
|
5
3
|
|
6
4
|
##
|
7
5
|
# Configuration options for the Statsig SDK.
|
8
6
|
class StatsigOptions
|
9
|
-
extend T::Sig
|
10
7
|
|
11
|
-
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
12
8
|
# Hash you can use to set environment variables that apply to all of your users in
|
13
9
|
# the same session and will be used for targeting purposes.
|
14
10
|
# eg. { "tier" => "development" }
|
15
11
|
attr_accessor :environment
|
16
12
|
|
17
|
-
sig { returns(String) }
|
18
13
|
# The base url used to make network calls to Statsig.
|
19
14
|
# default: https://statsigapi.net/v1
|
20
15
|
attr_accessor :api_url_base
|
21
16
|
|
22
17
|
# The base url used specifically to call download_config_specs.
|
23
18
|
# Takes precedence over api_url_base
|
24
|
-
sig { returns(T.any(String, NilClass)) }
|
25
19
|
attr_accessor :api_url_download_config_specs
|
26
20
|
|
27
|
-
sig { returns(T.any(Float, Integer)) }
|
28
21
|
# The interval (in seconds) to poll for changes to your Statsig configuration
|
29
22
|
# default: 10s
|
30
23
|
attr_accessor :rulesets_sync_interval
|
31
24
|
|
32
|
-
sig { returns(T.any(Float, Integer)) }
|
33
25
|
# The interval (in seconds) to poll for changes to your id lists
|
34
26
|
# default: 60s
|
35
27
|
attr_accessor :idlists_sync_interval
|
36
28
|
|
37
|
-
|
29
|
+
# Disable background syncing for rulesets
|
30
|
+
attr_accessor :disable_rulesets_sync
|
31
|
+
|
32
|
+
# Disable background syncing for id lists
|
33
|
+
attr_accessor :disable_idlists_sync
|
34
|
+
|
38
35
|
# How often to flush logs to Statsig
|
39
36
|
# default: 60s
|
40
37
|
attr_accessor :logging_interval_seconds
|
41
38
|
|
42
|
-
sig { returns(Integer) }
|
43
39
|
# The maximum number of events to batch before flushing logs to the server
|
44
40
|
# default: 1000
|
45
41
|
attr_accessor :logging_max_buffer_size
|
46
42
|
|
47
|
-
sig { returns(T::Boolean) }
|
48
43
|
# Restricts the SDK to not issue any network requests and only respond with default values (or local overrides)
|
49
44
|
# default: false
|
50
45
|
attr_accessor :local_mode
|
51
46
|
|
52
|
-
sig { returns(T.any(String, NilClass)) }
|
53
47
|
# A string that represents all rules for all feature gates, dynamic configs and experiments.
|
54
48
|
# It can be provided to bootstrap the Statsig server SDK at initialization in case your server runs
|
55
49
|
# into network issue or Statsig is down temporarily.
|
56
50
|
attr_accessor :bootstrap_values
|
57
51
|
|
58
|
-
sig { returns(T.any(Method, Proc, NilClass)) }
|
59
52
|
# A callback function that will be called anytime the rulesets are updated.
|
60
53
|
attr_accessor :rules_updated_callback
|
61
54
|
|
62
|
-
sig { returns(T.any(Statsig::Interfaces::IDataStore, NilClass)) }
|
63
55
|
# A class that extends IDataStore. Can be used to provide values from a
|
64
56
|
# common data store (like Redis) to initialize the Statsig SDK.
|
65
57
|
attr_accessor :data_store
|
66
58
|
|
67
|
-
sig { returns(Integer) }
|
68
59
|
# The number of threads allocated to syncing IDLists.
|
69
60
|
# default: 3
|
70
61
|
attr_accessor :idlist_threadpool_size
|
71
62
|
|
72
|
-
sig { returns(Integer) }
|
73
63
|
# The number of threads allocated to posting event logs.
|
74
64
|
# default: 3
|
75
65
|
attr_accessor :logger_threadpool_size
|
76
66
|
|
77
|
-
sig { returns(T::Boolean) }
|
78
67
|
# Should diagnostics be logged. These include performance metrics for initialize.
|
79
68
|
# default: false
|
80
69
|
attr_accessor :disable_diagnostics_logging
|
81
70
|
|
82
|
-
sig { returns(T::Boolean) }
|
83
71
|
# Statsig utilizes Sorbet (https://sorbet.org) to ensure type safety of the SDK. This includes logging
|
84
72
|
# to console when errors are detected. You can disable this logging by setting this flag to true.
|
85
73
|
# default: false
|
86
74
|
attr_accessor :disable_sorbet_logging_handlers
|
87
75
|
|
88
|
-
sig { returns(T.any(Integer, NilClass)) }
|
89
76
|
# Number of seconds before a network call is timed out
|
90
77
|
attr_accessor :network_timeout
|
91
78
|
|
92
|
-
sig { returns(Integer) }
|
93
79
|
# Number of times to retry sending a batch of failed log events
|
94
80
|
attr_accessor :post_logs_retry_limit
|
95
81
|
|
96
|
-
sig { returns(T.any(Method, Proc, Integer, NilClass)) }
|
97
82
|
# The number of seconds, or a function that returns the number of seconds based on the number of retries remaining
|
98
83
|
# which overrides the default backoff time between retries
|
99
84
|
attr_accessor :post_logs_retry_backoff
|
100
85
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
api_url_base: String,
|
105
|
-
api_url_download_config_specs: T.any(String, NilClass),
|
106
|
-
rulesets_sync_interval: T.any(Float, Integer),
|
107
|
-
idlists_sync_interval: T.any(Float, Integer),
|
108
|
-
logging_interval_seconds: T.any(Float, Integer),
|
109
|
-
logging_max_buffer_size: Integer,
|
110
|
-
local_mode: T::Boolean,
|
111
|
-
bootstrap_values: T.any(String, NilClass),
|
112
|
-
rules_updated_callback: T.any(Method, Proc, NilClass),
|
113
|
-
data_store: T.any(Statsig::Interfaces::IDataStore, NilClass),
|
114
|
-
idlist_threadpool_size: Integer,
|
115
|
-
logger_threadpool_size: Integer,
|
116
|
-
disable_diagnostics_logging: T::Boolean,
|
117
|
-
disable_sorbet_logging_handlers: T::Boolean,
|
118
|
-
network_timeout: T.any(Integer, NilClass),
|
119
|
-
post_logs_retry_limit: Integer,
|
120
|
-
post_logs_retry_backoff: T.any(Method, Proc, Integer, NilClass)
|
121
|
-
).void
|
122
|
-
end
|
86
|
+
# A storage adapter for persisted values. Can be used for sticky bucketing users in experiments.
|
87
|
+
# Implements Statsig::Interfaces::IUserPersistentStorage.
|
88
|
+
attr_accessor :user_persistent_storage
|
123
89
|
|
124
90
|
def initialize(
|
125
91
|
environment = nil,
|
126
|
-
api_url_base =
|
92
|
+
api_url_base = nil,
|
127
93
|
api_url_download_config_specs: nil,
|
128
94
|
rulesets_sync_interval: 10,
|
129
95
|
idlists_sync_interval: 60,
|
96
|
+
disable_rulesets_sync: false,
|
97
|
+
disable_idlists_sync: false,
|
130
98
|
logging_interval_seconds: 60,
|
131
99
|
logging_max_buffer_size: 1000,
|
132
100
|
local_mode: false,
|
@@ -139,12 +107,16 @@ class StatsigOptions
|
|
139
107
|
disable_sorbet_logging_handlers: false,
|
140
108
|
network_timeout: nil,
|
141
109
|
post_logs_retry_limit: 3,
|
142
|
-
post_logs_retry_backoff: nil
|
110
|
+
post_logs_retry_backoff: nil,
|
111
|
+
user_persistent_storage: nil
|
112
|
+
)
|
143
113
|
@environment = environment.is_a?(Hash) ? environment : nil
|
144
|
-
@api_url_base = api_url_base
|
145
|
-
@api_url_download_config_specs = api_url_download_config_specs
|
114
|
+
@api_url_base = api_url_base || 'https://statsigapi.net/v1'
|
115
|
+
@api_url_download_config_specs = api_url_download_config_specs || api_url_base || 'https://api.statsigcdn.com/v1'
|
146
116
|
@rulesets_sync_interval = rulesets_sync_interval
|
147
117
|
@idlists_sync_interval = idlists_sync_interval
|
118
|
+
@disable_rulesets_sync = disable_rulesets_sync
|
119
|
+
@disable_idlists_sync = disable_idlists_sync
|
148
120
|
@logging_interval_seconds = logging_interval_seconds
|
149
121
|
@logging_max_buffer_size = [logging_max_buffer_size, 1000].min
|
150
122
|
@local_mode = local_mode
|
@@ -158,5 +130,7 @@ class StatsigOptions
|
|
158
130
|
@network_timeout = network_timeout
|
159
131
|
@post_logs_retry_limit = post_logs_retry_limit
|
160
132
|
@post_logs_retry_backoff = post_logs_retry_backoff
|
133
|
+
@user_persistent_storage = user_persistent_storage
|
134
|
+
|
161
135
|
end
|
162
|
-
end
|
136
|
+
end
|
data/lib/statsig_user.rb
CHANGED
@@ -1,67 +1,49 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
1
|
require 'json'
|
2
|
+
require 'constants'
|
5
3
|
|
6
4
|
##
|
7
5
|
# The user object to be evaluated against your Statsig configurations (gates/experiments/dynamic configs).
|
8
6
|
class StatsigUser
|
9
|
-
extend T::Sig
|
10
7
|
|
11
|
-
sig { returns(T.any(String, NilClass)) }
|
12
8
|
# An identifier for this user. Evaluated against the User ID criteria. (https://docs.statsig.com/feature-gates/conditions#userid)
|
13
9
|
attr_accessor :user_id
|
14
10
|
|
15
|
-
sig { returns(T.any(String, NilClass)) }
|
16
11
|
# An identifier for this user. Evaluated against the Email criteria. (https://docs.statsig.com/feature-gates/conditions#email)
|
17
12
|
attr_accessor :email
|
18
13
|
|
19
|
-
sig { returns(T.any(String, NilClass)) }
|
20
14
|
# An IP address associated with this user. Evaluated against the IP Address criteria. (https://docs.statsig.com/feature-gates/conditions#ip)
|
21
15
|
attr_accessor :ip
|
22
16
|
|
23
|
-
sig { returns(T.any(String, NilClass)) }
|
24
17
|
# A user agent string associated with this user. Evaluated against Browser Version and Name (https://docs.statsig.com/feature-gates/conditions#browser-version)
|
25
18
|
attr_accessor :user_agent
|
26
19
|
|
27
|
-
sig { returns(T.any(String, NilClass)) }
|
28
20
|
# 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)
|
29
21
|
attr_accessor :country
|
30
22
|
|
31
|
-
sig { returns(T.any(String, NilClass)) }
|
32
23
|
# An locale for this user.
|
33
24
|
attr_accessor :locale
|
34
25
|
|
35
|
-
sig { returns(T.any(String, NilClass)) }
|
36
26
|
# The current app version the user is interacting with. Evaluated against the App Version criteria. (https://docs.statsig.com/feature-gates/conditions#app-version)
|
37
27
|
attr_accessor :app_version
|
38
28
|
|
39
|
-
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
40
29
|
# A Hash you can use to set environment variables that apply to this user. e.g. { "tier" => "development" }
|
41
30
|
attr_accessor :statsig_environment
|
42
31
|
|
43
|
-
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
44
32
|
# Any Custom IDs to associated with the user. (See https://docs.statsig.com/guides/experiment-on-custom-id-types)
|
45
33
|
attr_accessor :custom_ids
|
46
34
|
|
47
|
-
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
48
35
|
# Any value you wish to use in evaluation, but do not want logged with events, can be stored in this field.
|
49
36
|
attr_accessor :private_attributes
|
50
37
|
|
51
|
-
sig { returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
52
|
-
|
53
38
|
def custom
|
54
39
|
@custom
|
55
40
|
end
|
56
41
|
|
57
|
-
sig { params(value: T.any(T::Hash[String, T.untyped], NilClass)).void }
|
58
42
|
# Any custom fields for this user. Evaluated against the Custom criteria. (https://docs.statsig.com/feature-gates/conditions#custom)
|
59
43
|
def custom=(value)
|
60
44
|
@custom = value.is_a?(Hash) ? value : Hash.new
|
61
45
|
end
|
62
46
|
|
63
|
-
sig { params(user_hash: T.any(T::Hash[T.any(String, Symbol), T.untyped], NilClass)).void }
|
64
|
-
|
65
47
|
def initialize(user_hash)
|
66
48
|
the_hash = user_hash
|
67
49
|
begin
|
@@ -85,52 +67,79 @@ class StatsigUser
|
|
85
67
|
|
86
68
|
def serialize(for_logging)
|
87
69
|
hash = {
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
70
|
+
:userID => @user_id,
|
71
|
+
:email => @email,
|
72
|
+
:ip => @ip,
|
73
|
+
:userAgent => @user_agent,
|
74
|
+
:country => @country,
|
75
|
+
:locale => @locale,
|
76
|
+
:appVersion => @app_version,
|
77
|
+
:custom => @custom,
|
78
|
+
:statsigEnvironment => @statsig_environment,
|
79
|
+
:privateAttributes => @private_attributes,
|
80
|
+
:customIDs => @custom_ids,
|
99
81
|
}
|
100
82
|
if for_logging
|
101
|
-
hash.delete(
|
83
|
+
hash.delete(:privateAttributes)
|
102
84
|
end
|
103
85
|
hash.compact
|
104
86
|
end
|
105
87
|
|
106
|
-
def
|
107
|
-
{
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
88
|
+
def to_hash_without_stable_id
|
89
|
+
hash = {}
|
90
|
+
|
91
|
+
if @user_id != nil
|
92
|
+
hash[:userID] = @user_id
|
93
|
+
end
|
94
|
+
if @email != nil
|
95
|
+
hash[:email] = @email
|
96
|
+
end
|
97
|
+
if @ip != nil
|
98
|
+
hash[:ip] = @ip
|
99
|
+
end
|
100
|
+
if @user_agent != nil
|
101
|
+
hash[:userAgent] = @user_agent
|
102
|
+
end
|
103
|
+
if @country != nil
|
104
|
+
hash[:country] = @country
|
105
|
+
end
|
106
|
+
if @locale != nil
|
107
|
+
hash[:locale] = @locale
|
108
|
+
end
|
109
|
+
if @app_version != nil
|
110
|
+
hash[:appVersion] = @app_version
|
111
|
+
end
|
112
|
+
if @custom != nil
|
113
|
+
hash[:custom] = Statsig::HashUtils.sortHash(@custom)
|
114
|
+
end
|
115
|
+
if @statsig_environment != nil
|
116
|
+
hash[:statsigEnvironment] = @statsig_environment.clone.sort_by { |key| key }.to_h
|
117
|
+
end
|
118
|
+
if @private_attributes != nil
|
119
|
+
hash[:privateAttributes] = Statsig::HashUtils.sortHash(@private_attributes)
|
120
|
+
end
|
121
|
+
custom_ids = {}
|
122
|
+
if @custom_ids != nil
|
123
|
+
custom_ids = @custom_ids.clone
|
124
|
+
if custom_ids.key?("stableID")
|
125
|
+
custom_ids.delete("stableID")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
hash[:customIDs] = custom_ids.sort_by { |key| key }.to_h
|
129
|
+
return Statsig::HashUtils.djb2ForHash(hash.sort_by { |key| key }.to_h)
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_unit_id(id_type)
|
133
|
+
if id_type.is_a?(String) && id_type != Statsig::Const::CML_USER_ID
|
134
|
+
return nil unless @custom_ids.is_a? Hash
|
135
|
+
|
136
|
+
return @custom_ids[id_type] || @custom_ids[id_type.downcase]
|
137
|
+
end
|
138
|
+
@user_id
|
124
139
|
end
|
125
140
|
|
126
141
|
private
|
127
142
|
|
128
|
-
sig {
|
129
|
-
params(user_hash: T.any(T::Hash[T.any(String, Symbol), T.untyped], NilClass),
|
130
|
-
keys: T::Array[Symbol],
|
131
|
-
type: T.untyped)
|
132
|
-
.returns(T.untyped)
|
133
|
-
}
|
134
143
|
# Pulls fields from the user hash via Symbols and Strings
|
135
144
|
def from_hash(user_hash, keys, type)
|
136
145
|
if user_hash.nil?
|
@@ -146,5 +155,4 @@ class StatsigUser
|
|
146
155
|
|
147
156
|
nil
|
148
157
|
end
|
149
|
-
|
150
|
-
end
|
158
|
+
end
|
data/lib/ua_parser.rb
CHANGED
data/lib/uri_helper.rb
CHANGED
@@ -1,24 +1,16 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
|
-
|
5
1
|
class URIHelper
|
6
2
|
class URIBuilder
|
7
|
-
extend T::Sig
|
8
3
|
|
9
|
-
sig { returns(StatsigOptions) }
|
10
4
|
attr_accessor :options
|
11
5
|
|
12
|
-
sig { params(options: StatsigOptions).void }
|
13
6
|
def initialize(options)
|
14
7
|
@options = options
|
15
8
|
end
|
16
9
|
|
17
|
-
sig { params(endpoint: String).returns(String) }
|
18
10
|
def build_url(endpoint)
|
19
11
|
api = @options.api_url_base
|
20
|
-
if endpoint
|
21
|
-
api =
|
12
|
+
if endpoint.include?('download_config_specs')
|
13
|
+
api = @options.api_url_download_config_specs
|
22
14
|
end
|
23
15
|
unless api.end_with?('/')
|
24
16
|
api += '/'
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'statsig_options'
|
2
|
+
|
3
|
+
module Statsig
|
4
|
+
class UserPersistentStorageUtils
|
5
|
+
|
6
|
+
attr_accessor :cache
|
7
|
+
|
8
|
+
attr_accessor :storage
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
@storage = options.user_persistent_storage
|
12
|
+
@cache = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_user_persisted_values(user, id_type)
|
16
|
+
key = self.class.get_storage_key(user, id_type)
|
17
|
+
return @cache[key] unless @cache[key].nil?
|
18
|
+
|
19
|
+
return load_from_storage(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_from_storage(key)
|
23
|
+
return if @storage.nil?
|
24
|
+
|
25
|
+
begin
|
26
|
+
storage_values = @storage.load(key)
|
27
|
+
rescue StandardError => e
|
28
|
+
puts "Failed to load key (#{key}) from user_persisted_storage (#{e.message})"
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
|
32
|
+
unless storage_values.nil?
|
33
|
+
parsed_values = self.class.parse(storage_values)
|
34
|
+
unless parsed_values.nil?
|
35
|
+
@cache[key] = parsed_values
|
36
|
+
return @cache[key]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def save_to_storage(user, id_type, user_persisted_values)
|
43
|
+
return if @storage.nil?
|
44
|
+
|
45
|
+
key = self.class.get_storage_key(user, id_type)
|
46
|
+
stringified = self.class.stringify(user_persisted_values)
|
47
|
+
return if stringified.nil?
|
48
|
+
|
49
|
+
begin
|
50
|
+
@storage.save(key, stringified)
|
51
|
+
rescue StandardError => e
|
52
|
+
puts "Failed to save key (#{key}) to user_persisted_storage (#{e.message})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove_experiment_from_storage(user, id_type, config_name)
|
57
|
+
persisted_values = get_user_persisted_values(user, id_type)
|
58
|
+
unless persisted_values.nil?
|
59
|
+
persisted_values.delete(config_name)
|
60
|
+
save_to_storage(user, id_type, persisted_values)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_evaluation_to_user_persisted_values(user_persisted_values, config_name, evaluation)
|
65
|
+
if user_persisted_values.nil?
|
66
|
+
user_persisted_values = {}
|
67
|
+
end
|
68
|
+
user_persisted_values[config_name] = evaluation.to_hash
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def self.parse(values_string)
|
74
|
+
return JSON.parse(values_string)
|
75
|
+
rescue JSON::ParserError
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.stringify(values_object)
|
80
|
+
return JSON.generate(values_object)
|
81
|
+
rescue StandardError
|
82
|
+
return nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.get_storage_key(user, id_type)
|
86
|
+
"#{user.get_unit_id(id_type)}:#{id_type}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|