configcat 5.0.2 → 6.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 967461c3bdc865f2911296ecc35117dac6646c80e82496d74811b0649df689dc
4
- data.tar.gz: 7dd1b2d93dd82b8d279b9e3ac110e9ba564a14c775dfa132a044e749aa2f686b
3
+ metadata.gz: 6979b91044d036414367912894396e332a9fdd10b4239e72294a74949d8e5c82
4
+ data.tar.gz: fd58f1ea297a2fce1cf47e4bfe521724677617a32163d7b5bde3f2f80234482e
5
5
  SHA512:
6
- metadata.gz: 056f50b2304c5fdbcaaf39fc207d1f145d62892732f987a15fb8ad2e7267e57c92eb3ad6aeb2434424c7b26f4fa013350a26156d26fa2434969193e8f9d98d62
7
- data.tar.gz: 7ef97dac81e1a79d856880798bdfe897bc6de02bdf4ab4d7196320ef2c00d84ba69b6c4e6e08077de9f65c1191a1cb66220d2ecf4ad1e9e03f0e889cc00d83c1
6
+ metadata.gz: ae734ae5e04ae73c6ea7eb62c48a5633a943ac009c2f7b59729028a141932cdfdfceabbdef207d774153f586c0c418df939c0042ea288debe6bfccf877765ff7
7
+ data.tar.gz: 50b222dd87b113807f72ab24f3e0dfd01bb752639ab394094e40c34461e57ca0637d59038883b78d02dcdcfcd0469bfc21c6b6a5f74ab0b4658c17fcd8d7e8bc
@@ -1,17 +1,32 @@
1
1
  require 'configcat/interfaces'
2
2
 
3
3
  module ConfigCat
4
+ class NullConfigCache < ConfigCache
5
+ def initialize
6
+ @value = {}
7
+ end
8
+
9
+ def get(key)
10
+ return nil
11
+ end
12
+
13
+ def set(key, value)
14
+ # do nothing
15
+ end
16
+ end
17
+
4
18
  class InMemoryConfigCache < ConfigCache
5
- def initialize()
6
- @_value = {}
19
+ attr_reader :value
20
+ def initialize
21
+ @value = {}
7
22
  end
8
23
 
9
24
  def get(key)
10
- return @_value.fetch(key, nil)
25
+ return @value.fetch(key, nil)
11
26
  end
12
27
 
13
28
  def set(key, value)
14
- @_value[key] = value
29
+ @value[key] = value
15
30
  end
16
31
  end
17
32
  end
@@ -1,117 +1,189 @@
1
1
  require 'configcat/interfaces'
2
2
  require 'configcat/configcache'
3
+ require 'configcat/configcatoptions'
3
4
  require 'configcat/configfetcher'
4
- require 'configcat/autopollingcachepolicy'
5
- require 'configcat/manualpollingcachepolicy'
6
- require 'configcat/lazyloadingcachepolicy'
7
5
  require 'configcat/rolloutevaluator'
8
- require 'configcat/datagovernance'
6
+ require 'configcat/utils'
7
+ require 'configcat/configcatlogger'
8
+ require 'configcat/overridedatasource'
9
+ require 'configcat/configservice'
10
+ require 'configcat/evaluationdetails'
9
11
 
10
12
 
11
13
  module ConfigCat
12
14
  KeyValue = Struct.new(:key, :value)
13
15
  class ConfigCatClient
14
- @@sdk_keys = []
15
-
16
- def initialize(sdk_key,
17
- poll_interval_seconds: 60,
18
- max_init_wait_time_seconds: 5,
19
- on_configuration_changed_callback: nil,
20
- cache_time_to_live_seconds: 60,
21
- config_cache_class: nil,
22
- base_url: nil,
23
- proxy_address: nil,
24
- proxy_port: nil,
25
- proxy_user: nil,
26
- proxy_pass: nil,
27
- open_timeout: 10,
28
- read_timeout: 30,
29
- flag_overrides: nil,
30
- data_governance: DataGovernance::GLOBAL)
31
- if sdk_key === nil
32
- raise ConfigCatClientException, "SDK Key is required."
16
+ attr_reader :log, :hooks
17
+
18
+ @@lock = Mutex.new
19
+ @@instances = {}
20
+
21
+ # Creates a new or gets an already existing `ConfigCatClient` for the given `sdk_key`.
22
+ #
23
+ # :param sdk_key [String] ConfigCat SDK Key to access your configuration.
24
+ # :param options [ConfigCatOptions] Configuration for `ConfigCatClient`.
25
+ # :return [ConfigCatClient] the `ConfigCatClient` instance.
26
+ def self.get(sdk_key, options = nil)
27
+ @@lock.synchronize do
28
+ client = @@instances[sdk_key]
29
+ if client
30
+ if options
31
+ client.log.warn(3000, "There is an existing client instance for the specified SDK Key. " \
32
+ "No new client instance will be created and the specified options are ignored. " \
33
+ "Returning the existing client instance. SDK Key: '#{sdk_key}'.")
34
+ end
35
+ return client
36
+ end
37
+
38
+ options ||= ConfigCatOptions.new
39
+ client = ConfigCatClient.new(sdk_key, options)
40
+ @@instances[sdk_key] = client
41
+ return client
33
42
  end
43
+ end
34
44
 
35
- if @@sdk_keys.include?(sdk_key)
36
- ConfigCat.logger.warn("A ConfigCat Client is already initialized with sdk_key %s. "\
37
- "We strongly recommend you to use the ConfigCat Client as "\
38
- "a Singleton object in your application." % sdk_key)
39
- else
40
- @@sdk_keys.push(sdk_key)
45
+ # Closes all ConfigCatClient instances.
46
+ def self.close_all
47
+ @@lock.synchronize do
48
+ @@instances.each do |key, value|
49
+ value.send(:_close_resources)
50
+ end
51
+ @@instances.clear
41
52
  end
53
+ end
42
54
 
43
- @_sdk_key = sdk_key
44
- @_override_data_source = flag_overrides
55
+ private def initialize(sdk_key, options = ConfigCatOptions.new)
56
+ @hooks = options.hooks || Hooks.new
57
+ @log = ConfigCatLogger.new(@hooks)
58
+
59
+ if sdk_key === nil
60
+ raise ConfigCatClientException, "SDK Key is required."
61
+ end
45
62
 
46
- if config_cache_class
47
- @_config_cache = config_cache_class.new()
63
+ @_sdk_key = sdk_key
64
+ @_default_user = options.default_user
65
+ @_rollout_evaluator = RolloutEvaluator.new(@log)
66
+ if options.flag_overrides
67
+ @_override_data_source = options.flag_overrides.create_data_source(@log)
48
68
  else
49
- @_config_cache = InMemoryConfigCache.new()
69
+ @_override_data_source = nil
50
70
  end
51
71
 
52
- if !@_override_data_source.equal?(nil) && @_override_data_source.get_behaviour() == OverrideBehaviour::LOCAL_ONLY
72
+ config_cache = options.config_cache.nil? ? NullConfigCache.new : options.config_cache
73
+
74
+ if @_override_data_source && @_override_data_source.get_behaviour() == OverrideBehaviour::LOCAL_ONLY
53
75
  @_config_fetcher = nil
54
- @_cache_policy = nil
55
- elsif poll_interval_seconds > 0
56
- @_config_fetcher = CacheControlConfigFetcher.new(sdk_key, "a", base_url: base_url,
57
- proxy_address: proxy_address, proxy_port: proxy_port, proxy_user: proxy_user, proxy_pass: proxy_pass,
58
- open_timeout: open_timeout, read_timeout: read_timeout,
59
- data_governance: data_governance)
60
- @_cache_policy = AutoPollingCachePolicy.new(@_config_fetcher, @_config_cache, _get_cache_key(), poll_interval_seconds, max_init_wait_time_seconds, on_configuration_changed_callback)
61
- elsif cache_time_to_live_seconds > 0
62
- @_config_fetcher = CacheControlConfigFetcher.new(sdk_key, "l", base_url: base_url,
63
- proxy_address: proxy_address, proxy_port: proxy_port, proxy_user: proxy_user, proxy_pass: proxy_pass,
64
- open_timeout: open_timeout, read_timeout: read_timeout,
65
- data_governance: data_governance)
66
- @_cache_policy = LazyLoadingCachePolicy.new(@_config_fetcher, @_config_cache, _get_cache_key(), cache_time_to_live_seconds)
76
+ @_config_service = nil
67
77
  else
68
- @_config_fetcher = CacheControlConfigFetcher.new(sdk_key, "m", base_url: base_url,
69
- proxy_address: proxy_address, proxy_port: proxy_port, proxy_user: proxy_user, proxy_pass: proxy_pass,
70
- open_timeout: open_timeout, read_timeout: read_timeout,
71
- data_governance: data_governance)
72
- @_cache_policy = ManualPollingCachePolicy.new(@_config_fetcher, @_config_cache, _get_cache_key())
78
+ @_config_fetcher = ConfigFetcher.new(@_sdk_key,
79
+ @log,
80
+ options.polling_mode.identifier,
81
+ base_url: options.base_url,
82
+ proxy_address: options.proxy_address,
83
+ proxy_port: options.proxy_port,
84
+ proxy_user: options.proxy_user,
85
+ proxy_pass: options.proxy_pass,
86
+ open_timeout: options.open_timeout_seconds,
87
+ read_timeout: options.read_timeout_seconds,
88
+ data_governance: options.data_governance)
89
+
90
+ @_config_service = ConfigService.new(@sdk_key,
91
+ options.polling_mode,
92
+ @hooks,
93
+ @_config_fetcher,
94
+ @log,
95
+ config_cache,
96
+ options.offline)
73
97
  end
74
98
  end
75
99
 
76
- def get_value(key, default_value, user=nil)
77
- config = _get_settings()
78
- if config === nil
79
- ConfigCat.logger.warn("Evaluating get_value('%s') failed. Cache is empty. "\
80
- "Returning default_value in your get_value call: [%s]." % [key, default_value.to_s])
100
+ # Gets the value of a feature flag or setting identified by the given `key`.
101
+ #
102
+ # :param key [String] the identifier of the feature flag or setting.
103
+ # :param default_value in case of any failure, this value will be returned.
104
+ # :param user [User] the user object to identify the caller.
105
+ # :return the value.
106
+ def get_value(key, default_value, user = nil)
107
+ settings, fetch_time = _get_settings()
108
+ if settings.nil?
109
+ message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'."
110
+ @log.error(1000, message)
111
+ @hooks.invoke_on_flag_evaluated(EvaluationDetails.from_error(key, default_value, error: message))
81
112
  return default_value
82
113
  end
83
- value, variation_id = RolloutEvaluator.evaluate(key, user, default_value, nil, config)
84
- return value
114
+ details = _evaluate(key, user, default_value, nil, settings, fetch_time)
115
+ return details.value
85
116
  end
86
117
 
87
- def get_all_keys()
88
- config = _get_settings()
89
- if config === nil
90
- return []
118
+ # Gets the value and evaluation details of a feature flag or setting identified by the given `key`.
119
+ #
120
+ # :param key [String] the identifier of the feature flag or setting.
121
+ # :param default_value in case of any failure, this value will be returned.
122
+ # :param user [User] the user object to identify the caller.
123
+ # :return [EvaluationDetails] the evaluation details.
124
+ def get_value_details(key, default_value, user = nil)
125
+ settings, fetch_time = _get_settings()
126
+ if settings.nil?
127
+ message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'."
128
+ @log.error(1000, message)
129
+ details = EvaluationDetails.from_error(key, default_value, error: message)
130
+ @hooks.invoke_on_flag_evaluated(details)
131
+ return details
91
132
  end
92
- feature_flags = config.fetch(FEATURE_FLAGS, nil)
93
- if feature_flags === nil
133
+ details = _evaluate(key, user, default_value, nil, settings, fetch_time)
134
+ return details
135
+ end
136
+
137
+ # Gets all setting keys.
138
+ #
139
+ # :return list of keys.
140
+ def get_all_keys
141
+ settings, _ = _get_settings()
142
+ if settings === nil
143
+ @log.error(1000, "Config JSON is not present. Returning empty list.")
94
144
  return []
95
145
  end
96
- return feature_flags.keys
146
+ return settings.keys
97
147
  end
98
148
 
99
- def get_variation_id(key, default_variation_id, user=nil)
100
- config = _get_settings()
101
- if config === nil
102
- ConfigCat.logger.warn("Evaluating get_variation_id('%s') failed. Cache is empty. "\
103
- "Returning default_variation_id in your get_variation_id call: [%s]." %
104
- [key, default_variation_id.to_s])
149
+ # Gets the Variation ID (analytics) of a feature flag or setting based on it's key.
150
+ #
151
+ # :param key [String] the identifier of the feature flag or setting.
152
+ # :param default_variation_id in case of any failure, this value will be returned.
153
+ # :param user [User] the user object to identify the caller.
154
+ # :return the variation ID.
155
+ def get_variation_id(key, default_variation_id, user = nil)
156
+ ConfigCat.logger.warn("get_variation_id is deprecated and will be removed in a future major version. " \
157
+ "Please use [get_value_details] instead.")
158
+
159
+ settings, fetch_time = _get_settings()
160
+ if settings === nil
161
+ message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_variation_id` parameter that you specified in your application: '#{default_variation_id}'."
162
+ @log.error(1000, message)
163
+ @hooks.invoke_on_flag_evaluated(EvaluationDetails.from_error(key, nil, error: message,
164
+ variation_id: default_variation_id))
105
165
  return default_variation_id
106
166
  end
107
- value, variation_id = RolloutEvaluator.evaluate(key, user, nil, default_variation_id, config)
108
- return variation_id
167
+ details = _evaluate(key, user, nil, default_variation_id, settings, fetch_time)
168
+ return details.variation_id
109
169
  end
110
170
 
111
- def get_all_variation_ids(user: nil)
112
- keys = get_all_keys()
171
+ # Gets the Variation IDs (analytics) of all feature flags or settings.
172
+ #
173
+ # :param user [User] the user object to identify the caller.
174
+ # :return list of variation IDs
175
+ def get_all_variation_ids(user = nil)
176
+ ConfigCat.logger.warn("get_all_variation_ids is deprecated and will be removed in a future major version. " \
177
+ "Please use [get_value_details] instead.")
178
+
179
+ settings, _ = _get_settings()
180
+ if settings === nil
181
+ @log.error(1000, "Config JSON is not present. Returning empty list.")
182
+ return []
183
+ end
184
+
113
185
  variation_ids = []
114
- for key in keys
186
+ for key in settings.keys
115
187
  variation_id = get_variation_id(key, nil, user)
116
188
  if !variation_id.equal?(nil)
117
189
  variation_ids.push(variation_id)
@@ -120,20 +192,18 @@ module ConfigCat
120
192
  return variation_ids
121
193
  end
122
194
 
195
+ # Gets the key of a setting, and it's value identified by the given Variation ID (analytics)
196
+ #
197
+ # :param variation_id [String] variation ID
198
+ # :return key and value
123
199
  def get_key_and_value(variation_id)
124
- config = _get_settings()
125
- if config === nil
126
- ConfigCat.logger.warn("Evaluating get_variation_id('%s') failed. Cache is empty. Returning nil." % variation_id)
127
- return nil
128
- end
129
-
130
- feature_flags = config.fetch(FEATURE_FLAGS, nil)
131
- if feature_flags === nil
132
- ConfigCat.logger.warn("Evaluating get_key_and_value('%s') failed. Cache is empty. Returning None." % variation_id)
200
+ settings, _ = _get_settings()
201
+ if settings === nil
202
+ @log.error(1000, "Config JSON is not present. Returning nil.")
133
203
  return nil
134
204
  end
135
205
 
136
- for key, value in feature_flags
206
+ for key, value in settings
137
207
  if variation_id == value.fetch(VARIATION_ID, nil)
138
208
  return KeyValue.new(key, value[VALUE])
139
209
  end
@@ -152,12 +222,23 @@ module ConfigCat
152
222
  end
153
223
  end
154
224
  end
225
+
226
+ @log.error(2011, "Could not find the setting for the specified variation ID: '#{variation_id}'.")
155
227
  end
156
228
 
157
- def get_all_values(user: nil)
158
- keys = get_all_keys()
229
+ # Evaluates and returns the values of all feature flags and settings.
230
+ #
231
+ # :param user [User] the user object to identify the caller.
232
+ # :return dictionary of values
233
+ def get_all_values(user = nil)
234
+ settings, _ = _get_settings()
235
+ if settings === nil
236
+ @log.error(1000, "Config JSON is not present. Returning empty dictionary.")
237
+ return {}
238
+ end
239
+
159
240
  all_values = {}
160
- for key in keys
241
+ for key in settings.keys
161
242
  value = get_value(key, nil, user)
162
243
  if !value.equal?(nil)
163
244
  all_values[key] = value
@@ -166,47 +247,133 @@ module ConfigCat
166
247
  return all_values
167
248
  end
168
249
 
169
- def force_refresh()
170
- @_cache_policy.force_refresh()
250
+ # Gets the values along with evaluation details of all feature flags and settings.
251
+ #
252
+ # :param user [User] the user object to identify the caller.
253
+ # :return list of all evaluation details
254
+ def get_all_value_details(user = nil)
255
+ settings, fetch_time = _get_settings()
256
+ if settings.nil?
257
+ @log.error(1000, "Config JSON is not present. Returning empty list.")
258
+ return []
259
+ end
260
+
261
+ details_result = []
262
+ for key in settings.keys
263
+ details = _evaluate(key, user, nil, nil, settings, fetch_time)
264
+ details_result.push(details)
265
+ end
266
+
267
+ return details_result
268
+ end
269
+
270
+ # Initiates a force refresh on the cached configuration.
271
+ #
272
+ # :return [RefreshResult]
273
+ def force_refresh
274
+ return @_config_service.refresh if @_config_service
275
+
276
+ return RefreshResult(false,
277
+ "The SDK uses the LocalOnly flag override behavior which prevents making HTTP requests.")
278
+ end
279
+
280
+ # Sets the default user.
281
+ #
282
+ # :param user [User] the user object to identify the caller.
283
+ def set_default_user(user)
284
+ @_default_user = user
285
+ end
286
+
287
+ # Sets the default user to nil.
288
+ def clear_default_user
289
+ @_default_user = nil
171
290
  end
172
291
 
173
- def stop()
174
- @_cache_policy.stop() if @_cache_policy
175
- @_config_fetcher.close() if @_config_fetcher
176
- @@sdk_keys.delete(@_sdk_key)
292
+ # Configures the SDK to allow HTTP requests.
293
+ def set_online
294
+ if @_config_service
295
+ @_config_service.set_online
296
+ else
297
+ @log.warn(3202, "Client is configured to use the `LOCAL_ONLY` override behavior, thus `set_online()` has no effect.")
298
+ end
299
+ end
300
+
301
+ # Configures the SDK to not initiate HTTP requests and work only from its cache.
302
+ def set_offline
303
+ @_config_service.set_offline if @_config_service
304
+ end
305
+
306
+ # Returns true when the SDK is configured not to initiate HTTP requests, otherwise false.
307
+ def offline?
308
+ return @_config_service ? @_config_service.offline? : true
309
+ end
310
+
311
+ # Closes the underlying resources.
312
+ def close
313
+ @@lock.synchronize do
314
+ _close_resources
315
+ @@instances.delete(@_sdk_key)
316
+ end
177
317
  end
178
318
 
179
319
  private
180
320
 
181
- def _get_settings()
321
+ def _close_resources
322
+ @_config_service.close if @_config_service
323
+ @_config_fetcher.close if @_config_fetcher
324
+ @hooks.clear
325
+ end
326
+
327
+ def _get_settings
182
328
  if !@_override_data_source.nil?
183
329
  behaviour = @_override_data_source.get_behaviour()
184
330
  if behaviour == OverrideBehaviour::LOCAL_ONLY
185
- return @_override_data_source.get_overrides()
331
+ return @_override_data_source.get_overrides(), Utils::DISTANT_PAST
186
332
  elsif behaviour == OverrideBehaviour::REMOTE_OVER_LOCAL
187
- remote_settings = @_cache_policy.get()
333
+ remote_settings, fetch_time = @_config_service.get_settings()
188
334
  local_settings = @_override_data_source.get_overrides()
335
+ remote_settings ||= {}
336
+ local_settings ||= {}
189
337
  result = local_settings.clone()
190
- if remote_settings.key?(FEATURE_FLAGS) && local_settings.key?(FEATURE_FLAGS)
191
- result[FEATURE_FLAGS] = result[FEATURE_FLAGS].merge(remote_settings[FEATURE_FLAGS])
192
- end
193
- return result
338
+ result.update(remote_settings)
339
+ return result, fetch_time
194
340
  elsif behaviour == OverrideBehaviour::LOCAL_OVER_REMOTE
195
- remote_settings = @_cache_policy.get()
341
+ remote_settings, fetch_time = @_config_service.get_settings()
196
342
  local_settings = @_override_data_source.get_overrides()
343
+ remote_settings ||= {}
344
+ local_settings ||= {}
197
345
  result = remote_settings.clone()
198
- if remote_settings.key?(FEATURE_FLAGS) && local_settings.key?(FEATURE_FLAGS)
199
- result[FEATURE_FLAGS] = result[FEATURE_FLAGS].merge(local_settings[FEATURE_FLAGS])
200
- end
201
- return result
346
+ result.update(local_settings)
347
+ return result, fetch_time
202
348
  end
203
349
  end
204
- return @_cache_policy.get()
350
+ return @_config_service.get_settings()
205
351
  end
206
352
 
207
- def _get_cache_key()
353
+ def _get_cache_key
208
354
  return Digest::SHA1.hexdigest("ruby_" + CONFIG_FILE_NAME + "_" + @_sdk_key)
209
355
  end
210
356
 
357
+ def _evaluate(key, user, default_value, default_variation_id, settings, fetch_time)
358
+ user = user || @_default_user
359
+ value, variation_id, rule, percentage_rule, error = @_rollout_evaluator.evaluate(
360
+ key: key,
361
+ user: user,
362
+ default_value: default_value,
363
+ default_variation_id: default_variation_id,
364
+ settings: settings)
365
+
366
+ details = EvaluationDetails.new(key: key,
367
+ value: value,
368
+ variation_id: variation_id,
369
+ fetch_time: !fetch_time.nil? ? Time.at(fetch_time).utc : nil,
370
+ user: user,
371
+ is_default_value: error.nil? || error.empty? ? false : true,
372
+ error: error,
373
+ matched_evaluation_rule: rule,
374
+ matched_evaluation_percentage_rule: percentage_rule)
375
+ @hooks.invoke_on_flag_evaluated(details)
376
+ return details
377
+ end
211
378
  end
212
379
  end
@@ -0,0 +1,24 @@
1
+ module ConfigCat
2
+ class ConfigCatLogger
3
+ def initialize(hooks)
4
+ @hooks = hooks
5
+ end
6
+
7
+ def debug(message)
8
+ ConfigCat.logger.debug("[0] " + message)
9
+ end
10
+
11
+ def info(event_id, message)
12
+ ConfigCat.logger.info("[" + event_id.to_s + "] " + message)
13
+ end
14
+
15
+ def warn(event_id, message)
16
+ ConfigCat.logger.warn("[" + event_id.to_s + "] " + message)
17
+ end
18
+
19
+ def error(event_id, message)
20
+ @hooks.invoke_on_error(message)
21
+ ConfigCat.logger.error("[" + event_id.to_s + "] " + message)
22
+ end
23
+ end
24
+ end