statsig 1.25.2 → 1.33.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- # typed: true
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 = ['serverTime', 'configSyncTime', 'initTime', 'reason']
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
- 'gate' => gate_name,
43
- 'gateValue' => value.to_s,
44
- 'ruleID' => rule_id,
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
- 'config' => config_name,
61
- 'ruleID' => rule_id,
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
- 'config' => layer.name,
85
- 'ruleID' => layer.rule_id,
86
- 'allocatedExperiment' => allocated_experiment,
87
- 'parameterName' => parameter_name,
88
- 'isExplicitParameter' => String(is_explicit),
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 if not is_unique_exposure(user, $layer_exposure_event, metadata)
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 @options.disable_diagnostics_logging
101
- return if diagnostics.nil? || diagnostics.markers.empty?
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
- event.metadata = diagnostics.serialize
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
- loop do
113
- sleep @options.logging_interval_seconds
114
- flush_async
115
- @interval += 1
116
- @deduper.clear if @interval % 2 == 0
117
- end
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
- if @events.length == 0
136
- return
137
- end
138
- events_clone = @events
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
- @network.post_logs(flush_events)
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? or !@background_flush.alive?
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['reason'] = eval_details.reason
159
- event.metadata['configSyncTime'] = eval_details.config_sync_time
160
- event.metadata['initTime'] = eval_details.init_time
161
- event.metadata['serverTime'] = eval_details.server_time
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['is_manual_exposure']
170
- event.metadata['isManualExposure'] = 'true'
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
@@ -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
- sig { returns(T.any(Float, Integer)) }
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
- sig do
102
- params(
103
- environment: T.any(T::Hash[String, String], NilClass),
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 = 'https://statsigapi.net/v1',
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
- 'userID' => @user_id,
89
- 'email' => @email,
90
- 'ip' => @ip,
91
- 'userAgent' => @user_agent,
92
- 'country' => @country,
93
- 'locale' => @locale,
94
- 'appVersion' => @app_version,
95
- 'custom' => @custom,
96
- 'statsigEnvironment' => @statsig_environment,
97
- 'privateAttributes' => @private_attributes,
98
- 'customIDs' => @custom_ids,
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('privateAttributes')
83
+ hash.delete(:privateAttributes)
102
84
  end
103
85
  hash.compact
104
86
  end
105
87
 
106
- def value_lookup
107
- {
108
- 'userID' => @user_id,
109
- 'userid' => @user_id,
110
- 'user_id' => @user_id,
111
- 'email' => @email,
112
- 'ip' => @ip,
113
- 'userAgent' => @user_agent,
114
- 'useragent' => @user_agent,
115
- 'user_agent' => @user_agent,
116
- 'country' => @country,
117
- 'locale' => @locale,
118
- 'appVersion' => @app_version,
119
- 'appversion' => @app_version,
120
- 'app_version' => @app_version,
121
- 'custom' => @custom,
122
- 'privateAttributes' => @private_attributes,
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
@@ -1,3 +1,4 @@
1
+
1
2
  require 'user_agent_parser'
2
3
 
3
4
  module UAParser
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 == 'download_config_specs' && !@options.api_url_download_config_specs.nil?
21
- api = T.must(@options.api_url_download_config_specs)
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